How to Integrate GitHub Actions with Grawlr for Automated
Security Testing?
Continuous integration pipelines commonly run unit tests, code-quality checks, dependency scans and deployment validation. These checks are valuable, but they do not necessarily show how a running web application or web-based asset behaves when it receives malicious requests.
Grawlr adds dynamic and adaptive application security testing to the delivery pipeline. Instead of inspecting source code, Grawlr sends security test payloads to a running application and measures whether the application successfully blocks them. By integrating Grawlr with GitHub Actions, the development team can automatically trigger a security scan after a commit or a deployment, wait for the result, enforce a security threshold and stop the pipeline when too many attacks succeed.
The integration uses ordinary authenticated HTTP requests, so no custom GitHub Action is required.
How the Integration Works?
A typical GitHub Actions integration follows these steps:
- The application is deployed to a reachable staging, preview, or production environment.
- GitHub Actions sends a request to Grawlr to start a security scan.
- Grawlr queues the scan and returns a run identifier.
- GitHub Actions polls the run until the scan finishes.
- The workflow reads the security score and test results.
- The pipeline passes or fails according to the configured security policy.
Grawlr provides two CI/CD API endpoints:
- POST
/ci/v1/runsstarts an on-demand security scan. - GET
/ci/v1/runs/{run}returns the current scan status and, once completed, the result.
The API base URL is:
https://api.grawlr.com
Prerequisites
Before creating the GitHub Actions workflow, prepare the integration in the Grawlr dashboard.
You need:
- a Grawlr account with the CI/CD pipeline API enabled;
- an active and verified web application or web-based asset;
- at least 1 security test package assigned to that application;
- a CI/CD target created in the Grawlr dashboard;
- ... and a Grawlr CI/CD API key.
API keys and targets are created on the CI/CD section in the Grawlr dashboard.
The API key begins with:
grwl_ci_
The complete key is displayed only once when it is created. Store it immediately in a secure location.
The target is represented by a GUID, for example:
a1b2c3d4-e5f6-7890-abcd-ef1234567890
Grawlr only allows the pipeline API to start scans for targets that have already been configured and associated with an eligible verified web application or web-based asset.
Store the Grawlr Credentials in GitHub
The Grawlr API key must not be committed to the repository.
In GitHub, open:
Repository → Settings → Secrets and variables → Actions
Under Secrets, create:
GRAWLR_API_KEY
Store the complete grwl_ci_... API key as its value.
Under Variables, create:
GRAWLR_TARGET
Use the target GUID from the Grawlr CI/CD dashboard as its value.
The API key is sensitive and belongs in GitHub Secrets. The target GUID is an identifier rather than a credential, so it can normally be stored as a GitHub Actions variable.
They can then be accessed in the workflow as:
${{ secrets.GRAWLR_API_KEY }}
${{ vars.GRAWLR_TARGET }}
Authentication
Every Grawlr CI/CD API request must include the API key as a bearer token:
Authorization: Bearer grwl_ci_<your_api_key>
The CI/CD API does not use browser sessions or cookie authentication.
Understanding the Response Format
Every Grawlr CI/CD API response uses a JSON envelope.
A successful response looks like this:
{
"status": "OK",
"payload": {
"...": "..."
}
}
An unsuccessful response looks like this:
{
"status": "NOK",
"payload": "Description of the error"
}
The API also returns an appropriate HTTP status code, such as:
400for an invalid request;401for a missing or invalid API key;403when the CI/CD API is not enabled;404when a target or run cannot be found;429when the monthly CI/CD quota has been exceeded.
A reliable integration should check both the HTTP status code and the top-level status field.
Start a Grawlr Security Scan
To start a scan, send a POST request to:
https://api.grawlr.com/ci/v1/runs
The request body contains the target GUID:
{
"target": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
A direct curl request looks like this:
curl --request POST \
--url "https://api.grawlr.com/ci/v1/runs" \
--header "Authorization: Bearer grwl_ci_<your_api_key>" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data '{
"target": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}'
A successful response returns immediately:
{
"status": "OK",
"payload": {
"run": "f0e1d2c3-b4a5-6789-0abc-def123456789",
"status": "PENDING",
"target": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"queued_at": "2026-07-26 18:30:00"
}
}
The most important value is the run identifier:
"run": "f0e1d2c3-b4a5-6789-0abc-def123456789"
GitHub Actions must save this value and use it when checking the scan status.
Create the GitHub Actions Workflow
Create the following file in your repository:
.github/workflows/grawlr-security.yml
The workflow below can initially be triggered manually:
name: Grawlr Security Scan
on:
workflow_dispatch:
jobs:
grawlr-security:
runs-on: ubuntu-latest
timeout-minutes: 30
env:
GRAWLR_API_URL: https://api.grawlr.com
GRAWLR_TARGET: ${{ vars.GRAWLR_TARGET }}
steps:
- name: Start Grawlr security scan
id: start_grawlr
shell: bash
env:
GRAWLR_API_KEY: ${{ secrets.GRAWLR_API_KEY }}
run: |
set -euo pipefail
response_file=$(mktemp)
trap 'rm -f "${response_file}"' EXIT
http_code=$(curl \
--silent \
--show-error \
--output "${response_file}" \
--write-out "%{http_code}" \
--request POST \
--url "${GRAWLR_API_URL}/ci/v1/runs" \
--header "Authorization: Bearer ${GRAWLR_API_KEY}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "$(jq -n \
--arg target "${GRAWLR_TARGET}" \
'{target: $target}')")
response=$(cat "${response_file}")
if [ "${http_code}" -lt 200 ] || [ "${http_code}" -ge 300 ]; then
echo "Grawlr returned HTTP ${http_code}."
echo "${response}"
exit 1
fi
envelope_status=$(jq -r '.status // empty' <<< "${response}")
if [ "${envelope_status}" != "OK" ]; then
error_message=$(jq -r '.payload // "Unknown Grawlr error"' <<< "${response}")
echo "Grawlr rejected the request: ${error_message}"
exit 1
fi
run_id=$(jq -r '.payload.run // empty' <<< "${response}")
run_status=$(jq -r '.payload.status // empty' <<< "${response}")
if [ -z "${run_id}" ]; then
echo "Grawlr did not return a run identifier."
exit 1
fi
if [ "${run_status}" != "PENDING" ]; then
echo "Unexpected initial Grawlr status: ${run_status}"
exit 1
fi
echo "run=${run_id}" >> "${GITHUB_OUTPUT}"
echo "Grawlr scan queued successfully."
echo "Run: ${run_id}"
Using jq -n to construct the request body avoids problems caused by manually escaping JSON inside the shell script.
Poll Until the Scan Finishes
A Grawlr scan is asynchronous. However starting a run queues it for immediate execution.
A run can have the following statuses:
PENDINGmeans the scan is queued.RUNNINGmeans a worker is executing the scan.COMPLETEDmeans the scan finished and the result is available.FAILEDmeans the scan could not be executed successfully.CANCELLEDmeans the run was cancelled.
A background worker normally picks up on-demand runs approximately once per minute. The workflow should therefore wait between polling requests.
Add the following step after the scan creation step:
- name: Wait for Grawlr scan
id: wait_grawlr
shell: bash
env:
GRAWLR_API_KEY: ${{ secrets.GRAWLR_API_KEY }}
GRAWLR_RUN: ${{ steps.start_grawlr.outputs.run }}
run: |
set -euo pipefail
max_attempts=30
polling_interval=30
for attempt in $(seq 1 "${max_attempts}"); do
echo "Checking Grawlr run ${GRAWLR_RUN}."
echo "Polling attempt ${attempt}/${max_attempts}."
response_file=$(mktemp)
http_code=$(curl \
--silent \
--show-error \
--output "${response_file}" \
--write-out "%{http_code}" \
--request GET \
--url "${GRAWLR_API_URL}/ci/v1/runs/${GRAWLR_RUN}" \
--header "Authorization: Bearer ${GRAWLR_API_KEY}" \
--header "Accept: application/json")
response=$(cat "${response_file}")
rm -f "${response_file}"
if [ "${http_code}" -lt 200 ] || [ "${http_code}" -ge 300 ]; then
echo "Grawlr returned HTTP ${http_code} while polling."
echo "${response}"
exit 1
fi
envelope_status=$(jq -r '.status // empty' <<< "${response}")
if [ "${envelope_status}" != "OK" ]; then
error_message=$(jq -r '.payload // "Unknown Grawlr error"' <<< "${response}")
echo "Could not retrieve the Grawlr run: ${error_message}"
exit 1
fi
run_status=$(jq -r '.payload.status // empty' <<< "${response}")
echo "Current Grawlr status: ${run_status}"
case "${run_status}" in
PENDING|RUNNING)
sleep "${polling_interval}"
;;
COMPLETED)
echo "${response}" > grawlr-result.json
score=$(jq -r '.payload.score' <<< "${response}")
tests=$(jq -r '.payload.tests' <<< "${response}")
passed=$(jq -r '.payload.passed' <<< "${response}")
failed=$(jq -r '.payload.failed' <<< "${response}")
blocked=$(jq -r '.payload.blocked' <<< "${response}")
echo "score=${score}" >> "${GITHUB_OUTPUT}"
echo "tests=${tests}" >> "${GITHUB_OUTPUT}"
echo "passed=${passed}" >> "${GITHUB_OUTPUT}"
echo "failed=${failed}" >> "${GITHUB_OUTPUT}"
echo "blocked=${blocked}" >> "${GITHUB_OUTPUT}"
echo "Grawlr scan completed."
echo "Score: ${score}"
echo "Tests: ${tests}"
echo "Blocked: ${blocked}"
echo "Failed: ${failed}"
exit 0
;;
FAILED)
echo "${response}" > grawlr-result.json
error_code=$(jq -r '.payload.error // "scan_execution_failed"' <<< "${response}")
echo "Grawlr could not execute the scan."
echo "Execution error: ${error_code}"
exit 1
;;
CANCELLED)
echo "${response}" > grawlr-result.json
echo "The Grawlr scan was cancelled."
exit 1
;;
*)
echo "Unexpected Grawlr run status: ${run_status}"
exit 1
;;
esac
done
echo "Grawlr did not complete within the configured polling period."
exit 1
With 30 attempts and a 30-second interval, the workflow allows approximately 15 minutes for the scan to begin and finish.
Larger test packages may require a longer timeout.
Understanding the Completed Result
A completed scan returns a payload similar to this:
{
"status": "OK",
"payload": {
"run": "f0e1d2c3-b4a5-6789-0abc-def123456789",
"status": "COMPLETED",
"target": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"queued_at": "2026-07-26 18:30:00",
"started_at": "2026-07-26 18:31:05",
"completed_at": "2026-07-26 18:34:22",
"score": 92.5,
"tests": 40,
"passed": 37,
"failed": 3,
"blocked": 37
}
}
The fields have the following meanings:
scoreis the overall security score from 0 to 100.testsis the total number of attacks attempted.passedis the number of attacks blocked by the application.blockedis an alias forpassed.failedis the number of attacks that got through.
A higher passed or blocked value is good.
A higher failed value is bad because it indicates that more attack attempts were not stopped.
The full human-readable report and PDF are not returned through the CI/CD API. Detailed findings and reports can be opened from the Grawlr dashboard.
Gatekeeping the Pipeline
Once the run reaches COMPLETED, GitHub Actions can decide whether the pipeline should continue.
The strictest rule is:
failed == 0
This means that every tested attack must be blocked.
However, a team introducing Grawlr into an existing application may initially need a more gradual rule. For example, the pipeline may continue as long as no more than five tests fail:
failed <= 5
A minimum security score can be enforced at the same time:
score >= 80
Add the following evaluation step:
- name: Apply Grawlr security gate
shell: bash
env:
GRAWLR_SCORE: ${{ steps.wait_grawlr.outputs.score }}
GRAWLR_FAILED: ${{ steps.wait_grawlr.outputs.failed }}
MAX_FAILED_TESTS: "5"
MIN_SECURITY_SCORE: "80"
run: |
set -euo pipefail
echo "Applying Grawlr pipeline policy."
echo "Security score: ${GRAWLR_SCORE}"
echo "Failed tests: ${GRAWLR_FAILED}"
echo "Minimum score: ${MIN_SECURITY_SCORE}"
echo "Maximum failed tests: ${MAX_FAILED_TESTS}"
if [ "${GRAWLR_FAILED}" -gt "${MAX_FAILED_TESTS}" ]; then
echo "Security gate failed."
echo "Grawlr found ${GRAWLR_FAILED} successful attacks."
echo "The configured maximum is ${MAX_FAILED_TESTS}."
exit 1
fi
if ! awk \
-v score="${GRAWLR_SCORE}" \
-v minimum="${MIN_SECURITY_SCORE}" \
'BEGIN { exit !(score >= minimum) }'
then
echo "Security gate failed."
echo "Grawlr score ${GRAWLR_SCORE} is below ${MIN_SECURITY_SCORE}."
exit 1
fi
echo "Grawlr security gate passed."
The score comparison uses awk because Grawlr scores may contain decimal values such as 92.5. Standard Bash integer comparisons do not reliably handle decimal numbers.
Example Gatekeeping Policies
A strict production policy could use:
MAX_FAILED_TESTS: "0" MIN_SECURITY_SCORE: "90"
This blocks deployment when any tested attack succeeds.
A progressive adoption policy could use:
MAX_FAILED_TESTS: "5" MIN_SECURITY_SCORE: "80"
This allows a limited number of existing findings while still preventing severely insecure releases.
During the initial integration, a team may temporarily use monitoring-only mode:
- name: Report Grawlr result
run: |
echo "Grawlr score: ${{ steps.wait_grawlr.outputs.score }}"
echo "Failed tests: ${{ steps.wait_grawlr.outputs.failed }}"
Monitoring-only mode should normally be temporary. Once the baseline is understood, the team can introduce stricter thresholds.
What “No More Than Five Failed” Means?
A policy allowing no more than five failed tests means:
failed <= 5
The deployment is allowed when the result contains zero, one, two, three, four, or five failed tests.
The pipeline should be blocked when the result contains six or more failed tests.
The Bash condition is:
if [ "${failed}" -gt 5 ]; then
exit 1
fi
This is different from requiring fewer than five failures, which would mean failed < 5.
Distinguish Security Findings from Execution Failures
A completed scan with three failed tests might look like this:
{
"status": "COMPLETED",
"failed": 3
}
This means the scan ran successfully, but three attack attempts got through.
An execution failure might look like this:
{
"status": "FAILED",
"error": "scan_execution_failed",
"failed": 0,
"score": null
}
This means the scan itself did not complete and these results must not be treated as equivalent.
A FAILED run is an operational or infrastructure failure. The zero in the failed field does not mean the application passed the security test. No valid security conclusion can be made because the scan did not finish.
The workflow should normally fail in both situations, but it should report a different reason
- A security gate failure means the scan completed, but too many attacks succeeded.
- An execution failure means Grawlr could not complete the scan.
External Notifications
Grawlr can notify external communication and incident-management channels when a scan finishes.
When external notifications are enabled, the CI/CD-triggered scan result is sent to all configured and enabled notification channels. For example, when Slack notifications are enabled, the configured Slack channel receives the result of the scan. Similarly when several integrations are enabled, all configured channels are notified according to their notification settings. This can include services such as Slack, Mattermost, Discord, PagerDuty and other supported destinations.
The GitHub Actions workflow therefore does not necessarily need its own Slack-specific or webhook-specific notification step. GitHub Actions remains responsible for deciding whether the pipeline passes or fails. Grawlr handles distributing the scan result to the configured external channels.
This provides several advantages:
- Notification settings can be managed centrally in Grawlr.
- The same notification policy can be used across several repositories.
- External service credentials do not need to be duplicated in GitHub.
- Security and operations teams can receive results without regularly checking GitHub Actions.
Add the Result to the GitHub Job Summary
GitHub Actions supports a formatted job summary.
Add the following step:
- name: Add Grawlr result to job summary
if: always() && steps.wait_grawlr.outputs.tests != ''
shell: bash
run: |
{
echo "## Grawlr Security Scan"
echo ""
echo "Score: ${{ steps.wait_grawlr.outputs.score }}"
echo ""
echo "Tests: ${{ steps.wait_grawlr.outputs.tests }}"
echo ""
echo "Blocked: ${{ steps.wait_grawlr.outputs.blocked }}"
echo ""
echo "Failed: ${{ steps.wait_grawlr.outputs.failed }}"
} >> "${GITHUB_STEP_SUMMARY}"
This version avoids a Markdown table and is easier to paste into rich-text editors.
Upload the JSON Result as an Artifact
The API response can also be retained as a GitHub Actions artifact:
- name: Upload Grawlr result
if: always()
uses: actions/upload-artifact@v4
with:
name: grawlr-security-result
path: grawlr-result.json
if-no-files-found: warn
retention-days: 14
The if: always() condition allows the upload step to run even when the security gate fails.
The artifact contains the API response rather than the complete Grawlr report. Detailed findings remain available in the Grawlr dashboard.
Run Grawlr After Deployment
Grawlr requires a running application and the job should therefore normally follow a successful application deployment:
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Deploy to staging
run: ./scripts/deploy-staging.sh
grawlr-security:
needs: deploy-staging
runs-on: ubuntu-latest
steps:
- name: Start Grawlr security scan
run: |
# Grawlr API request
The needs: deploy-staging setting prevents the security scan from starting before the application has been deployed.
For preview or staging environments, it can also be useful to wait until a health endpoint responds:
- name: Wait for staging application
shell: bash
run: |
set -euo pipefail
for attempt in $(seq 1 30); do
if curl \
--silent \
--show-error \
--fail \
"https://staging.example.com/health" \
> /dev/null
then
echo "Staging application is ready."
exit 0
fi
echo "Waiting for staging application."
sleep 10
done
echo "Staging application did not become ready."
exit 1
Common API Errors
Invalid or Missing API Key
The API may return:
{
"status": "NOK",
"payload": "Invalid or missing API key"
}
The HTTP status is 401.
Check that:
GRAWLR_API_KEYexists in GitHub Secrets;- the complete API key was copied;
- the key has not been revoked;
- and the authorization header uses
Bearer.
CI/CD API Not Enabled
The API may return:
{
"status": "NOK",
"payload": "CI/CD pipeline API is not enabled for this account"
}
The HTTP status is 403.
The CI/CD pipeline feature must be enabled for the Grawlr account.
Target Is Missing
The API may return:
{
"status": "NOK",
"payload": "target is required"
}
Check that GRAWLR_TARGET exists and is available to the workflow.
Target Not Found
The API may return:
{
"status": "NOK",
"payload": "Target not found"
}
The target may have been deleted, copied incorrectly, or created under another customer account.
Web Application Is Not Active or Verified
The API may return:
{
"status": "NOK",
"payload": "Target website is not active/verified"
}
Open the Grawlr dashboard and verify that the application is active and eligible for testing.
Package Is Not Assigned
The API may return:
{
"status": "NOK",
"payload": "Package is not assigned to this website"
}
The application must have a security test package assigned before the CI/CD API can queue a run.
Monthly Quota Exceeded
The API may return:
{
"status": "NOK",
"payload": "Monthly CI/CD scan quota exceeded"
}
The HTTP status is 429.
The account has reached its monthly on-demand CI/CD scan allowance.
Recommended Rollout Strategy
A practical rollout can happen in three stages.
Stage 1: Visibility
Run Grawlr after staging deployments and report the result without blocking releases.
This establishes the current baseline and helps the team understand which findings are already present.
Stage 2: Limited Gatekeeping
Start with a policy such as:
failed <= 5 score >= 80
This prevents severe regressions while giving the team time to address existing findings.
Stage 3: Strict Enforcement
Move toward a stricter policy such as:
failed == 0 score >= 90
The appropriate score threshold depends on the application, the assigned security test package, and the organization’s release policy.
Conclusion
Integrating Grawlr with GitHub Actions adds dynamic and adaptive security testing directly to the CI/CD process.
The workflow starts an on-demand scan with:
POST /ci/v1/runs
It then polls:
GET /ci/v1/runs/{run}
until the run is completed or fails.
Once the scan is completed, GitHub Actions can use the result to enforce deployment rules. A strict pipeline can require zero failed tests, while a progressive policy can require a minimum security score. When external notifications are enabled in Grawlr, the scan result is also sent to all configured notification channels, such as Slack. This keeps engineering, security, and operations teams informed without requiring every GitHub repository to maintain separate notification integrations.
The end result is an automated and repeatable security validation that can stop a deployment when the application no longer meets its required security standard.