A GitHub Action for running automated tests when scanners are modified in the scanner registry.
This action detects which scanners have been modified in a pull request and runs their associated tests across CI/CD providers. It enables scanner authors to validate their changes before merging.
- uses: boostsecurityio/scanner-registry-testing/test-action@main
with:
provider: github-actions
provider-config: |
{
"token": "${{ secrets.TEST_RUNNER_TOKEN }}",
"owner": "boostsecurityio",
"repo": "test-runner-github",
"workflow_id": "scanner-test.yml"
}
registry-path: "."
registry-repo: "boostsecurityio/scanner-registry"
base-ref: "origin/main"| Input | Required | Description |
|---|---|---|
provider |
Yes | CI/CD provider key (e.g., github-actions, gitlab-ci, azure-devops, bitbucket) |
provider-config |
Yes | JSON configuration for the provider (see provider sections below) |
registry-path |
No | Path to registry root (default: .) |
registry-repo |
No | Registry repository in org/repo format (default: extracted from git remote) |
base-ref |
No | Base git reference for diff (default: origin/main) |
| Output | Description |
|---|---|
results |
JSON object with test results including status, total, passed, failed, and scanners array |
The action can also be run directly via CLI:
poetry run python -m scan_test_action.cli \
--provider github-actions \
--provider-config '{"token": "...", "owner": "...", "repo": "...", "workflow_id": "..."}' \
--registry-path . \
--registry-repo org/scanner-registry \
--base-ref origin/main- Python 3.12+
- Poetry
- Docker (for module tests)
- act (for module tests)
The TestOrchestrator coordinates test execution on a single CI/CD provider. It receives pre-loaded test definitions and orchestrates their execution in parallel.
┌──────────────────┐ ┌──────────────────┐
│ Test Definitions │ │ Provider │
│ (pre-loaded) │────▶│ Dispatch │
└──────────────────┘ └────────┬─────────┘
│
┌──────────────────┐ │
│ Results │◀───────┘
│ Aggregation │
└──────────────────┘
orchestrator = TestOrchestrator(provider=my_provider)
results = await orchestrator.run_tests(
test_definitions={"org/scanner": test_def},
registry_repo="org/registry",
registry_ref="abc123",
)- Dispatch Tests: Send all tests to the provider in parallel using
asyncio.gather() - Wait for Completion: Each dispatched test is polled until complete or timeout
- Aggregate Results: Collect results into
ScannerResultobjects
Each scanner produces a ScannerResult containing:
scanner_id: The scanner identifier (e.g., "boostsecurityio/trivy-fs")results: Sequence ofTestResultobjects from the provider
- Empty test definitions: Returns empty list immediately
- Dispatch failure: Returns error result with exception message
- Wait failure: Returns error result with exception message
- Partial failure: Successful results are preserved alongside error results
The action compares the PR branch against the base branch to identify modified scanners. A scanner is considered modified if any file under scanners/<org>/<scanner>/ has changed.
Only scanners with a tests.yaml file are tested. Scanners without test definitions are skipped.
- Compares git refs (e.g.,
origin/mainvsHEAD) to find changed files - Extracts unique scanner identifiers from paths like
scanners/org/scanner/file.yaml - Filters to only scanners that have a
tests.yamlfile - Returns the list of testable scanners
In CI environments like GitHub Actions, branch refs often exist with an origin/ prefix. The detector automatically tries both forms when resolving references.
Scanner tests are defined in tests.yaml files within each scanner directory:
version: "1.0"
tests:
- name: "Smoke test - source code"
type: "source-code"
source:
url: "https://github.com/OWASP/NodeGoat.git"
ref: "main"
- name: "Container image scan"
type: "container-image"
source:
url: "https://github.com/example/docker-app.git"
ref: "v1.0"
scan_paths:
- "app"
- "api"| Field | Required | Description |
|---|---|---|
version |
Yes | Schema version (currently "1.0") |
tests |
Yes | List of test specifications |
tests[].name |
Yes | Human-readable test name |
tests[].type |
Yes | Either "source-code" or "container-image" |
tests[].source.url |
Yes | Git repository URL (HTTPS) |
tests[].source.ref |
Yes | Git reference (branch, tag, or commit SHA) |
tests[].scan_paths |
No | Paths to scan (default: ["."]) |
tests[].timeout |
No | Test timeout (default: "5m") |
Each test is expanded into matrix entries for CI execution. A test with multiple scan_paths creates one matrix entry per path, enabling parallel execution.
The action supports multiple CI/CD providers through a common interface. Each provider handles dispatching tests and polling for results.
Providers are registered as entry points in pyproject.toml and loaded dynamically by key:
from scan_test_action.providers.loading import load_provider_manifest
# Load provider by key (e.g., from config or CLI)
manifest = load_provider_manifest("github-actions")
# Create provider instance using the factory
async with manifest.provider_factory(config) as provider:
results = await provider.dispatch_scanner_tests(...)Each manifest contains:
config_cls: The provider's Pydantic configuration classprovider_factory: The async context manager factory (typicallyProvider.from_config)
To add a new provider:
- Create the provider module under
scan_test_action/providers/<name>/ - Add a
manifest.pywith theProviderManifestinstance - Register it in
pyproject.tomlunder[tool.poetry.plugins."scan_test_action.providers"] - Run
poetry installto update the entry points
Providers implement PipelineProvider[T], a generic interface where T is the dispatch state type:
@dataclass(frozen=True, kw_only=True)
class MyProvider(PipelineProvider[str]):
# Configuration fields...
async def dispatch_scanner_tests(...) -> str:
# Dispatch tests, return state for polling
return run_id
async def poll_status(dispatch_state: str) -> Sequence[TestResult] | None:
# Return results when complete, None when still running
return results if complete else NoneThe generic type allows providers to pass any state between dispatch and poll - from a simple run ID to complex objects with multiple identifiers.
The base class provides wait_for_completion() which handles polling with timeout. Providers only implement the dispatch and poll methods.
The GitHubActionsProvider dispatches tests to a GitHub Actions workflow and polls for completion.
When calling this action from a GitHub workflow, the provider configuration is passed as a JSON object:
- uses: boostsecurityio/scanner-registry-testing/test-action@main
with:
provider: github-actions
provider-config: |
{
"token": "${{ secrets.TEST_RUNNER_TOKEN }}",
"owner": "boostsecurityio",
"repo": "test-runner-github",
"workflow_id": "scanner-test.yml",
"ref": "main"
}| Field | Required | Description |
|---|---|---|
token |
Yes | GitHub token with workflow permissions |
owner |
Yes | Repository owner |
repo |
Yes | Repository name |
workflow_id |
Yes | Workflow file name or ID |
ref |
No | Branch to run workflow on (default: "main") |
The provider generates a unique dispatch ID (UUID) for each workflow dispatch. This ID is passed as a workflow input and should be displayed in the workflow's run-name. The provider uses this ID to reliably find the correct workflow run among concurrent executions.
Test runner workflow requirements:
- Accept
dispatch_idas a workflow input - Include the dispatch ID in
run-name(e.g.,run-name: "[${{ inputs.dispatch_id }}] Scanner Tests")
The provider uses a single aiohttp.ClientSession for its lifetime, configured with the base URL and authorization headers. Use the from_config async context manager to ensure proper cleanup.
The GitLabCIProvider dispatches tests to a GitLab CI pipeline and polls for completion.
- uses: boostsecurityio/scanner-registry-testing/test-action@main
with:
provider: gitlab-ci
provider-config: |
{
"trigger_token": "${{ secrets.GITLAB_TRIGGER_TOKEN }}",
"api_token": "${{ secrets.GITLAB_API_TOKEN }}",
"project_id": "boostsecurityio/test-runner-gitlab",
"ref": "main"
}| Field | Required | Description |
|---|---|---|
trigger_token |
Yes | GitLab Pipeline Trigger Token for dispatching pipelines |
api_token |
Yes | GitLab Project Access Token (Guest role, read_api scope) for polling |
project_id |
Yes | Project ID (numeric) or path (e.g., "org/project") |
ref |
No | Branch to run pipeline on (default: "main") |
The provider uses two separate tokens with minimal privileges:
-
Pipeline Trigger Token (
trigger_token): Created in GitLab project settings under CI/CD > Pipeline trigger tokens. Used to dispatch pipelines via the/trigger/pipelineendpoint without requiring any header authentication. -
Project Access Token (
api_token): Created in GitLab project settings under Access Tokens. Requires:- Role: Guest (minimum privilege needed)
- Scope:
read_api
Used with Bearer token authentication to poll pipeline status via the standard API endpoints.
The provider passes test configuration as pipeline variables:
SCANNER_ID: Scanner being tested (e.g., "boostsecurityio/trivy-fs")REGISTRY_REF: Git commit SHA of the registryREGISTRY_REPO: Registry repository in org/repo formatMATRIX_TESTS: JSON array of test matrix entries
The project_id can be either:
- A numeric ID (e.g.,
"12345") - A URL-encoded path (e.g.,
"boostsecurityio%2Ftest-runner") - An unencoded path (e.g.,
"boostsecurityio/test-runner") - the provider will URL-encode it automatically
The AzureDevOpsProvider dispatches tests to an Azure DevOps pipeline and polls for completion.
- uses: boostsecurityio/scanner-registry-testing/test-action@main
with:
provider: azure-devops
provider-config: |
{
"token": "${{ secrets.AZURE_PAT }}",
"organization": "my-org",
"project": "my-project",
"pipeline_id": 42
}| Field | Required | Description |
|---|---|---|
token |
Yes | Azure DevOps Personal Access Token with Build permissions |
organization |
Yes | Azure DevOps organization name |
project |
Yes | Azure DevOps project name |
pipeline_id |
Yes | Pipeline definition ID (numeric) |
The provider passes test configuration as pipeline template parameters:
SCANNER_ID: Scanner being tested (e.g., "boostsecurityio/trivy-fs")REGISTRY_REF: Git commit SHA of the registryREGISTRY_REPO: Registry repository in org/repo formatMATRIX_TESTS: JSON array of test matrix entries
Azure DevOps uses Basic authentication with an empty username and the Personal Access Token as the password. The token is automatically base64-encoded by the provider.
The BitbucketProvider dispatches tests to a Bitbucket Pipeline and polls for completion.
- uses: boostsecurityio/scanner-registry-testing/test-action@main
with:
provider: bitbucket
provider-config: |
{
"token": "${{ secrets.BITBUCKET_TOKEN }}",
"workspace": "my-workspace",
"repo_slug": "test-runner-bitbucket",
"branch": "main"
}| Field | Required | Description |
|---|---|---|
token |
Yes | Bitbucket OAuth access token |
workspace |
Yes | Bitbucket workspace slug |
repo_slug |
Yes | Repository slug |
branch |
No | Branch to run pipeline on (default: "main") |
The provider passes test configuration as custom pipeline variables:
SCANNER_ID: Scanner being tested (e.g., "boostsecurityio/trivy-fs")REGISTRY_REF: Git commit SHA of the registryREGISTRY_REPO: Registry repository in org/repo formatMATRIX_TESTS: JSON array of test matrix entries
The provider triggers a custom pipeline with the pattern test-scanner. Your bitbucket-pipelines.yml should define this custom pipeline:
pipelines:
custom:
test-scanner:
- step:
name: Run Scanner Tests
script:
- echo "Running tests for $SCANNER_ID"Bitbucket uses OAuth Bearer token authentication. The token should have permissions to trigger pipelines on the target repository.