pipeline {
agent any
environment {
// 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'
BINARLY_CLIENT_ID = credentials('binarly-client-id')
BINARLY_CLIENT_SECRET = credentials('binarly-client-secret')
BINARLY_PRODUCT_ID = credentials('binarly-product-id')
}
stages {
stage('Build Firmware') {
steps {
sh 'make firmware'
}
}
stage('Set up Authentication') {
steps {
// Prints a new access token. Tokens expire after 30 minutes, so
// every API call below requests a new one.
writeFile file: 'binarly-token.sh', text: '''
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'
'''
}
}
stage('Upload Firmware') {
steps {
script {
// Generate upload URL. "set +x" keeps the token out of the build log.
def uploadRes = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
curl -sS --fail-with-body -X POST -H "Authorization: Bearer $TOKEN" \\
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/tempFiles:generateUploadUrl"
''',
returnStdout: true
)
def uploadData = readJSON(text: uploadRes)
// Upload binary
sh "curl -sS --fail-with-body -X PUT --data-binary @./build/firmware.bin '${uploadData.uploadUrl}'"
// Create image and start scan
writeJSON file: 'binarly-image.json', json: [
tempFileId: uploadData.id,
name : "Jenkins Build ${BUILD_NUMBER}".toString(),
version : "1.0.${BUILD_NUMBER}".toString(),
filename : 'firmware.bin'
]
def imageRes = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
curl -sS --fail-with-body -X POST \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d @binarly-image.json \\
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images"
''',
returnStdout: true
)
env.IMAGE_ID = readJSON(text: imageRes).id
echo "Image uploaded: ${env.IMAGE_ID}"
}
}
}
stage('Wait for Scan') {
steps {
script {
def status = 'new'
while (status == 'new' || status == 'running') {
sleep 30
def scanRes = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
curl -sS --fail-with-body -H "Authorization: Bearer $TOKEN" \\
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images/$IMAGE_ID/scans"
''',
returnStdout: true
)
status = readJSON(text: scanRes).scans[0].latestScanState.type
echo "Scan status: ${status}"
}
if (status != 'done') {
error "Binarly scan did not finish successfully: ${status}"
}
echo 'Scan completed successfully'
}
}
}
stage('Verify Results') {
steps {
script {
echo 'Binarly Security Verification'
def failOnStatus = env.FAIL_ON_STATUS ?: 'new,inProgress'
echo "Failing on statuses: ${failOnStatus}"
// Find the image uploaded before this one, to compare against
def imagesRes = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
curl -sS --fail-with-body -H "Authorization: Bearer $TOKEN" \\
"$BINARLY_API_URL/api/v4/products/$BINARLY_PRODUCT_ID/images"
''',
returnStdout: true
)
def images = readJSON(text: imagesRes).images
def created = images.find { it.id == env.IMAGE_ID }?.createTime
def previousId = null
if (created) {
def older = images.findAll { it.createTime < created }
if (older) {
previousId = older.max { it.createTime }.id
}
}
if (previousId) {
echo "Comparing: ${previousId} -> ${env.IMAGE_ID}"
def counts = [:]
for (side in ['left', 'both', 'right']) {
withEnv(["PREVIOUS_ID=${previousId}", "SIDE=${side}"]) {
counts[side] = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
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":"'"$SIDE"'","comparator":"equals"}]}' \\
"$BINARLY_API_URL/api/v4/grids/findings:gridList" | jq -e '.total'
''',
returnStdout: true
).trim()
}
}
echo "Resolved: ${counts.left} | Unchanged: ${counts.both} | New: ${counts.right}"
}
// Check for findings matching configured statuses (using 'in' comparator with array)
def statusesJson = '[' + failOnStatus.split(',').collect { '"' + it.trim() + '"' }.join(',') + ']'
def totalActionable
withEnv(["STATUSES_JSON=${statusesJson}"]) {
totalActionable = sh(
script: '''
set +x
TOKEN=$(sh ./binarly-token.sh)
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'
''',
returnStdout: true
).trim().toInteger()
}
echo "Statuses checked: ${failOnStatus}"
echo "Total actionable findings: ${totalActionable}"
if (totalActionable > 0) {
error "BUILD FAILED: ${totalActionable} actionable finding(s)"
}
echo 'BUILD PASSED: No actionable findings'
}
}
}
}
}