Panther informational experimental python

GitHub Workflow Using Self-Hosted Runner

Detects when a GitHub Actions workflow runs on a self-hosted runner.

View Source

Detection Logic

def rule(event):
    # Only check completed workflow jobs
    if event.get("action") != "completed":
        return False

    # GitHub-hosted runners always have "GitHub Actions" in the runner_name
    # Self-hosted runners cannot use this reserved name
    runner_name = event.deep_get("workflow_job", "runner_name", default="")

    # Must have a runner name and it must not be GitHub-hosted
    if not runner_name:
        return False

    return not runner_name.startswith("GitHub Actions")


def title(event):
    workflow_name = event.deep_get("workflow_job", "name", default="Unknown Workflow")
    repo_name = event.deep_get("repository", "full_name", default="Unknown Repository")
    runner_name = event.deep_get("workflow_job", "runner_name", default="Unknown Runner")

    return f"Self-hosted runner '{runner_name}' used in workflow '{workflow_name}' for {repo_name}"


def alert_context(event):
    workflow_job = event.get("workflow_job", {})
    repository = event.get("repository", {})

    return {
        "workflow_name": workflow_job.get("name"),
        "workflow_job_id": workflow_job.get("id"),
        "workflow_run_id": workflow_job.get("run_id"),
        "workflow_url": workflow_job.get("html_url"),
        "repository": repository.get("full_name"),
        "repository_private": repository.get("private"),
        "repository_visibility": repository.get("visibility"),
        "head_branch": workflow_job.get("head_branch"),
        "head_sha": workflow_job.get("head_sha"),
        "conclusion": workflow_job.get("conclusion"),
        "runner_name": workflow_job.get("runner_name"),
        "runner_group_name": workflow_job.get("runner_group_name"),
        "runner_id": workflow_job.get("runner_id"),
        "runner_group_id": workflow_job.get("runner_group_id"),
        "actor": event.deep_get("sender", "login"),
    }


def severity(event):
    # Public or forkable repos with self-hosted runners have a medium risk
    repo_visibility = event.deep_get("repository", "visibility")
    allow_forking = event.deep_get("repository", "allow_forking", default=False)
    is_private = event.deep_get("repository", "private", default=True)

    if repo_visibility == "public" or (not is_private) or allow_forking:
        return "MEDIUM"

    # Private, non-forkable repos are low risk
    return "INFO"

Field Validations

Loading…

Comments (0)

Loading comments...