curl and jq. It works with any CI/CD system that supports shell scripts.
Prerequisites
curlandjqinstalled- Environment variables configured
Complete Script
Save the script asbinarly-scan.sh, add the verification block to its end, and make it executable with chmod +x binarly-scan.sh.
#!/bin/bash
set -eo pipefail
# Configuration (Set these as environment variables in your CI)
# BINARLY_CLIENT_ID="your-client-id"
# BINARLY_CLIENT_SECRET="your-client-secret"
# BINARLY_AUTH_URL="https://auth-{slug}.binarly.cloud/realms/BinarlyRealm/protocol/openid-connect/token"
# BINARLY_API_URL="https://dashboard-{slug}.binarly.cloud"
# BINARLY_FIRMWARE_FILE="./firmware.bin"
# BINARLY_PRODUCT_ID="01JQPGDX8QW0YJ0XEKV1EVG534"
# 1. Authenticate (M2M)
# Access tokens expire after 30 minutes, so the script requests a new one
# before each group of API calls.
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'
}
echo "LOG: Authenticating..."
TOKEN=$(get_token)
# 2. Upload Firmware (3-Step Flow)
echo "LOG: Generating Upload 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')
echo "LOG: Uploading Binary..."
curl -sS --fail-with-body -X PUT --data-binary @"${BINARLY_FIRMWARE_FILE}" "${UPLOAD_URL}"
echo "LOG: Creating Image..."
# Generate unique image name with timestamp to avoid conflicts
IMAGE_NAME="CI Build ${BUILD_NUMBER:-0}-$(date +%Y%m%d%H%M%S)"
IMAGE_PAYLOAD=$(jq -n \
--arg tempFileId "${TEMP_ID}" \
--arg name "${IMAGE_NAME}" \
--arg version "1.0.${BUILD_NUMBER:-0}" \
--arg filename "$(basename "${BINARLY_FIRMWARE_FILE}")" \
'{tempFileId: $tempFileId, name: $name, version: $version, filename: $filename}')
IMAGE_RES=$(curl -sS --fail-with-body -X POST \
-H "Authorization: Bearer ${TOKEN}" \
-H "Content-Type: application/json" \
-d "${IMAGE_PAYLOAD}" \
"${BINARLY_API_URL}/api/v4/products/${BINARLY_PRODUCT_ID}/images") || {
echo "ERROR: Upload failed. Response: ${IMAGE_RES}"
exit 1
}
IMAGE_ID=$(echo "$IMAGE_RES" | jq -er '.id')
echo "LOG: Scan started for Image ID: ${IMAGE_ID}"
# 3. Poll for Results
echo "LOG: Waiting for scan to complete..."
while true; do
sleep 30
TOKEN=$(get_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 "LOG: Scan status: ${STATUS}"
# Check if scan is complete
if [ "$STATUS" == "done" ]; then
echo "SUCCESS: Security analysis completed."
break
elif [ "$STATUS" == "failed" ] || [ "$STATUS" == "cancelled" ]; then
echo "ERROR: Scan ${STATUS}"
exit 1
elif [ "$STATUS" != "new" ] && [ "$STATUS" != "running" ]; then
echo "ERROR: Unexpected scan status: ${STATUS}"
exit 1
fi
# Continue waiting for new/running states
done
Environment Variables
| Variable | Description |
|---|---|
BINARLY_CLIENT_ID | Keycloak API Client ID |
BINARLY_CLIENT_SECRET | Keycloak API Client Secret |
BINARLY_AUTH_URL | Authentication endpoint |
BINARLY_API_URL | API base URL |
BINARLY_PRODUCT_ID | Target product ID |
BINARLY_FIRMWARE_FILE | Path to firmware binary |
BUILD_NUMBER | CI build number (optional) |
FAIL_ON_STATUS | Finding statuses that fail the build (optional, default new,inProgress) |
Usage
# Set environment variables
export BINARLY_CLIENT_ID="my-api-client"
export BINARLY_CLIENT_SECRET="secret123"
export BINARLY_AUTH_URL="https://auth-{slug}.binarly.cloud/realms/BinarlyRealm/protocol/openid-connect/token"
export BINARLY_API_URL="https://dashboard-{slug}.binarly.cloud"
export BINARLY_PRODUCT_ID="01JQPGDX8QW0YJ0XEKV1EVG534"
export BINARLY_FIRMWARE_FILE="./build/firmware.bin"
export BUILD_NUMBER="42"
# Run script
./binarly-scan.sh
4. Verify Results
After the scan completes, verify the results and optionally fail the build based on findings. Add this block to the end ofbinarly-scan.sh. It uses get_token and IMAGE_ID from the steps above, so it doesn’t work as a separate script.
# 4. Verify Results
# Optional: FAIL_ON_STATUS (default: "new,inProgress")
FAIL_ON_STATUS="${FAIL_ON_STATUS:-new,inProgress}"
echo "Binarly Security Verification"
echo "Failing on statuses: ${FAIL_ON_STATUS}"
echo ""
TOKEN=$(get_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"