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
defaults:
run:
# Runs steps with bash -eo pipefail, so a failed API call stops the step
shell: bash
env:
BINARLY_CLIENT_ID: ${{ secrets.BINARLY_CLIENT_ID }}
BINARLY_CLIENT_SECRET: ${{ secrets.BINARLY_CLIENT_SECRET }}
BINARLY_PRODUCT_ID: ${{ secrets.BINARLY_PRODUCT_ID }}
steps:
- uses: actions/checkout@v4
- name: Build Firmware
run: make firmware # Your build command
- name: Set up Binarly authentication
run: |
# Access tokens expire after 30 minutes, so each step requests a new one
cat > "$RUNNER_TEMP/binarly-auth.sh" <<'EOF'
get_token() {
curl -sS --fail-with-body \
-H "Content-Type: application/x-www-form-urlencoded" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_id=${BINARLY_CLIENT_ID}" \
--data-urlencode "client_secret=${BINARLY_CLIENT_SECRET}" \
"$BINARLY_AUTH_URL" | jq -er '.access_token'
}
EOF
- name: Upload Firmware
id: upload
run: |
source "$RUNNER_TEMP/binarly-auth.sh"
TOKEN=$(get_token)
echo "::add-mask::$TOKEN"
# Generate pre-signed URL
UPLOAD_RES=$(curl -sS --fail-with-body -X POST -H "Authorization: Bearer $TOKEN" \
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/tempFiles:generateUploadUrl")
UPLOAD_URL=$(echo "$UPLOAD_RES" | jq -er '.uploadUrl')
TEMP_ID=$(echo "$UPLOAD_RES" | jq -er '.id')
# Upload binary
curl -sS --fail-with-body -X PUT --data-binary @./build/firmware.bin "$UPLOAD_URL"
# Create image and start scan
IMAGE_RES=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$(jq -n --arg id "$TEMP_ID" --arg run "$GITHUB_RUN_NUMBER" \
'{tempFileId: $id, name: ("GitHub Build " + $run), version: ("1.0." + $run), filename: "firmware.bin"}')" \
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images")
IMAGE_ID=$(echo "$IMAGE_RES" | jq -er '.id')
echo "image_id=$IMAGE_ID" >> "$GITHUB_OUTPUT"
- name: Wait for Scan
env:
IMAGE_ID: ${{ steps.upload.outputs.image_id }}
run: |
source "$RUNNER_TEMP/binarly-auth.sh"
STATUS="new"
while [ "$STATUS" == "new" ] || [ "$STATUS" == "running" ]; do
sleep 30
TOKEN=$(get_token)
echo "::add-mask::$TOKEN"
SCAN_RES=$(curl -sS --fail-with-body -H "Authorization: Bearer $TOKEN" \
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images/$IMAGE_ID/scans")
STATUS=$(echo "$SCAN_RES" | jq -r '.scans[0].latestScanState.type')
echo "Scan status: $STATUS"
done
if [ "$STATUS" != "done" ]; then
echo "Scan did not finish successfully: $STATUS"
exit 1
fi
echo "Scan completed successfully"
- name: Verify Results
env:
IMAGE_ID: ${{ steps.upload.outputs.image_id }}
FAIL_ON_STATUS: "new,inProgress" # Configurable: new,inProgress,rejected,remediated
run: |
echo "Binarly Security Verification"
echo "Failing on statuses: ${FAIL_ON_STATUS}"
echo ""
source "$RUNNER_TEMP/binarly-auth.sh"
TOKEN=$(get_token)
echo "::add-mask::$TOKEN"
# Find the image uploaded before this one, to compare against
IMAGES_RES=$(curl -sS --fail-with-body -H "Authorization: Bearer ${TOKEN}" \
"${BINARLY_API_URL}/api/v4/products/${BINARLY_PRODUCT_ID}/images")
PREVIOUS_ID=$(echo "$IMAGES_RES" | jq -r --arg id "$IMAGE_ID" '
(.images[] | select(.id == $id) | .createTime) as $created
| [.images[] | select(.createTime < $created)]
| sort_by(.createTime) | last | .id // empty')
if [ -n "$PREVIOUS_ID" ]; then
echo "Comparing images:"
echo " Previous: ${PREVIOUS_ID}"
echo " Latest: ${IMAGE_ID}"
echo ""
# Count RESOLVED findings (only in previous image = left side)
RESOLVED=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filters": [
{"field": "compareLeftImageId", "value": "'"${PREVIOUS_ID}"'", "comparator": "equals"},
{"field": "compareRightImageId", "value": "'"${IMAGE_ID}"'", "comparator": "equals"},
{"field": "compareSide", "value": "left", "comparator": "equals"}
]
}' \
"${BINARLY_API_URL}/api/v4/grids/findings:gridList" | jq -e '.total')
# Count UNCHANGED findings (present in both images)
UNCHANGED=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filters": [
{"field": "compareLeftImageId", "value": "'"${PREVIOUS_ID}"'", "comparator": "equals"},
{"field": "compareRightImageId", "value": "'"${IMAGE_ID}"'", "comparator": "equals"},
{"field": "compareSide", "value": "both", "comparator": "equals"}
]
}' \
"${BINARLY_API_URL}/api/v4/grids/findings:gridList" | jq -e '.total')
# Count NEW findings (only in latest image = right side)
NEW_ISSUES=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filters": [
{"field": "compareLeftImageId", "value": "'"${PREVIOUS_ID}"'", "comparator": "equals"},
{"field": "compareRightImageId", "value": "'"${IMAGE_ID}"'", "comparator": "equals"},
{"field": "compareSide", "value": "right", "comparator": "equals"}
]
}' \
"${BINARLY_API_URL}/api/v4/grids/findings:gridList" | jq -e '.total')
echo "Comparison Statistics:"
printf " Resolved from previous: %6d\n" "$RESOLVED"
printf " Unchanged (still present): %6d\n" "$UNCHANGED"
printf " Newly introduced: %6d\n" "$NEW_ISSUES"
echo ""
fi
# Count findings in this image with the configured statuses
# Convert comma-separated statuses to a JSON array
STATUSES_JSON=$(echo "$FAIL_ON_STATUS" | tr -d ' ' | tr ',' '\n' | jq -R . | jq -s .)
TOTAL_ACTIONABLE=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d '{
"filters": [
{"field": "imageId", "value": "'"${IMAGE_ID}"'", "comparator": "equals"},
{"field": "issueStatus", "value": '"${STATUSES_JSON}"', "comparator": "in"}
]
}' \
"${BINARLY_API_URL}/api/v4/grids/findings:gridList" | jq -e '.total')
echo "Statuses checked: ${FAIL_ON_STATUS}"
echo "Total actionable findings: ${TOTAL_ACTIONABLE}"
echo ""
# Pass/Fail decision
if [ "$TOTAL_ACTIONABLE" -gt 0 ]; then
echo "BUILD FAILED: ${TOTAL_ACTIONABLE} actionable finding(s)"
exit 1
fi
echo "BUILD PASSED: No actionable findings"