Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 120 additions & 0 deletions .github/workflows/auto-bounty-payout.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
name: Auto Bounty Payout

on:
pull_request:
types: [closed]

jobs:
process-bounty:
# Only run if PR was merged (not just closed)
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
issues: write
pull-requests: read
contents: read

steps:
- name: Extract linked issues from PR
id: extract_issues
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_BODY: ${{ github.event.pull_request.body }}
run: |
pr_number="${{ github.event.pull_request.number }}"

echo "PR #$pr_number was merged"
echo "PR Body: $PR_BODY"

# Extract issue numbers from PR body
# Looks for patterns like "Fixes #123", "Closes #456", "Resolves #789"
issue_numbers=$(echo "$PR_BODY" | grep -oiE "(close[sd]?|fix(e[sd])?|resolve[sd]?) #[0-9]+" | grep -oE "#[0-9]+" | sed 's/#//' || echo "")

if [ -z "$issue_numbers" ]; then
echo "No linked issues found in PR body"
echo "has_issues=false" >> $GITHUB_OUTPUT
else
echo "Found linked issues: $issue_numbers"
echo "has_issues=true" >> $GITHUB_OUTPUT
# Convert to JSON array
issue_array=$(echo "$issue_numbers" | jq -R -s -c 'split("\n") | map(select(length > 0))')
echo "issue_numbers=$issue_array" >> $GITHUB_OUTPUT
fi

- name: Check for bounty labels and process payment
if: steps.extract_issues.outputs.has_issues == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ISSUE_NUMBERS: ${{ steps.extract_issues.outputs.issue_numbers }}
run: |
echo "Processing issues: $ISSUE_NUMBERS"

# Parse JSON array
for issue_num in $(echo "$ISSUE_NUMBERS" | jq -r '.[]'); do
echo "Checking issue #$issue_num for bounty labels..."

# Get issue details
issue_data=$(gh api repos/${{ github.repository }}/issues/$issue_num)

# Check if issue has a dollar label (bounty)
has_bounty=$(echo "$issue_data" | jq -r '.labels[] | select(.name | startswith("$")) | .name' | head -n 1)

if [ -n "$has_bounty" ]; then
echo "Issue #$issue_num has bounty label: $has_bounty"

# Construct issue URL
issue_url="https://github.com/${{ github.repository }}/issues/$issue_num"
pr_url="${{ github.event.pull_request.html_url }}"

echo "Sending bounty payout request to BLT API..."

# Call the BLT bounty payout API
# BLT_API_URL must be set in repository secrets/variables
# This prevents accidental production calls during testing
# Add BLT_API_TOKEN to repository secrets for authentication
if [ -z "${{ secrets.BLT_API_URL }}" ]; then
echo "❌ BLT_API_URL is not set. Refusing to call production API by default."
echo "Please set BLT_API_URL in repository secrets/variables."
echo "For production: https://blt.owasp.org"
echo "For testing: your test instance URL"
exit 1
fi
api_url="${{ secrets.BLT_API_URL }}"

echo "Using API URL: $api_url"

response=$(curl -X POST \
-H "Content-Type: application/json" \
-H "X-BLT-API-TOKEN: ${{ secrets.BLT_API_TOKEN }}" \
-d "{\"issue_url\": \"$issue_url\", \"pr_url\": \"$pr_url\"}" \
"${api_url}/api/bounty_payout/" \
-w "\n%{http_code}" -s)

http_code=$(echo "$response" | tail -n 1)
response_body=$(echo "$response" | head -n -1)

echo "API Response (HTTP $http_code): $response_body"

if [ "$http_code" -eq 200 ]; then
echo "✅ Successfully initiated bounty payout for issue #$issue_num"

# Add a comment to the issue
gh issue comment $issue_num --body "🎉 Bounty payout has been initiated for this issue! The payment will be processed via GitHub Sponsors."
else
echo "⚠️ Failed to process bounty payout for issue #$issue_num"
echo "Response: $response_body"

# Add a comment about the failure
gh issue comment $issue_num --body "⚠️ Automated bounty payout failed. Please contact an administrator. Error: $response_body"
fi
else
echo "Issue #$issue_num does not have a bounty label, skipping..."
fi
done

- name: Summary
if: always()
run: |
echo "Auto bounty payout workflow completed"
echo "PR: ${{ github.event.pull_request.html_url }}"
echo "Merged by: ${{ github.event.pull_request.merged_by.login }}"
16 changes: 16 additions & 0 deletions blt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -631,3 +631,19 @@
}
THROTTLE_WINDOW = 60 # 60 seconds (1 minute)
THROTTLE_EXEMPT_PATHS = ["/admin/", "/static/", "/media/"]

# Bounty Payout Configuration
# API token for authenticating bounty payout requests
BLT_API_TOKEN = os.environ.get("BLT_API_TOKEN")
# GitHub username that sponsors will be created from (default: DonnieBLT)
GITHUB_SPONSOR_USERNAME = os.environ.get("GITHUB_SPONSOR_USERNAME", "DonnieBLT")
# Allowlist of repositories eligible for automated bounty payouts (prevents budget drain attacks)
# Format: {"owner/repo", "another-org/another-repo"}
BLT_ALLOWED_BOUNTY_REPOS = {
os.environ.get("BLT_ALLOWED_BOUNTY_REPO_1", "OWASP-BLT/BLT"),
}
# Add additional repos from environment if configured
if os.environ.get("BLT_ALLOWED_BOUNTY_REPO_2"):
BLT_ALLOWED_BOUNTY_REPOS.add(os.environ.get("BLT_ALLOWED_BOUNTY_REPO_2"))
if os.environ.get("BLT_ALLOWED_BOUNTY_REPO_3"):
BLT_ALLOWED_BOUNTY_REPOS.add(os.environ.get("BLT_ALLOWED_BOUNTY_REPO_3"))
Comment on lines +651 to +666
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Document these settings for production deployment.

The bounty payout configuration requires environment variables to function properly. Ensure deployment documentation includes:

  • BLT_API_TOKEN: Required for authenticating bounty payout requests from GitHub Actions
  • GITHUB_SPONSOR_USERNAME: The GitHub account that will sponsor users (currently defaults to "DonnieBLT")
  • BLT_ALLOWED_BOUNTY_REPO_1, BLT_ALLOWED_BOUNTY_REPO_2, BLT_ALLOWED_BOUNTY_REPO_3: Repositories eligible for automated bounty payouts (prevents unauthorized repos from draining budget)

Consider verifying these are documented in the deployment guide or README:


🏁 Script executed:

#!/bin/bash
# Check if deployment documentation mentions these new environment variables
rg -i "BLT_API_TOKEN|GITHUB_SPONSOR_USERNAME|BLT_ALLOWED_BOUNTY_REPO" --type=md

Length of output: 0


The markdown search returned no results. Let me conduct a broader search to verify whether these environment variables are documented anywhere in the codebase, including non-markdown formats and environment configuration files.


🏁 Script executed:

#!/bin/bash
# Search more broadly for documentation of these variables
# Check all file types, not just markdown
echo "=== Searching entire codebase for these variables ==="
rg -i "BLT_API_TOKEN|GITHUB_SPONSOR_USERNAME|BLT_ALLOWED_BOUNTY_REPO" -C2

echo ""
echo "=== Checking for .env files or documentation templates ==="
fd -e env -e sample -e example -e txt -e rst | head -20

echo ""
echo "=== Looking for docs or documentation directories ==="
fd -t d "doc|guide|config" | head -20

Length of output: 2678


Let me verify what deployment documentation exists:


🏁 Script executed:

#!/bin/bash
# Check deployment and README documentation
echo "=== Files in docs/ directory ==="
find docs -type f | head -30

echo ""
echo "=== Root README files ==="
ls -la | grep -i readme

echo ""
echo "=== Checking for deployment/setup docs ==="
rg -i "deployment|setup|environment|configure" docs/ --type=md 2>/dev/null | head -20

Length of output: 1571


Let me check the specific content of the setup documentation to see if these bounty variables are mentioned:


🏁 Script executed:

#!/bin/bash
# Check Setup.md for bounty variables
echo "=== Searching Setup.md for bounty-related variables ==="
rg "BLT_API_TOKEN|GITHUB_SPONSOR|ALLOWED_BOUNTY" docs/Setup.md

echo ""
echo "=== Searching bot-setup.md for bounty-related variables ==="
rg "BLT_API_TOKEN|GITHUB_SPONSOR|ALLOWED_BOUNTY" docs/bot-setup.md

echo ""
echo "=== Check if .env.example exists ==="
find . -name ".env.example" -o -name "*.env*"

echo ""
echo "=== View Setup.md environment variables section ==="
sed -n '/environment variables/,/^## /Ip' docs/Setup.md | head -40

Length of output: 679


🏁 Script executed:

#!/bin/bash
# Check .env.example for bounty configuration
echo "=== Full content of .env.example ==="
cat .env.example

Length of output: 1412


Add bounty payout environment variables to .env.example and deployment documentation.

The bounty payout configuration is missing from .env.example and deployment documentation. Since these variables are used in website/views/organization.py for critical bounty payout functionality, they must be documented:

  • BLT_API_TOKEN: Required for authenticating bounty payout requests from GitHub Actions
  • GITHUB_SPONSOR_USERNAME: The GitHub account that will sponsor users (defaults to "DonnieBLT")
  • BLT_ALLOWED_BOUNTY_REPO_1/2/3: Repositories eligible for automated bounty payouts (prevents unauthorized repos from draining budget)

Add these to .env.example with example values and document them in docs/Setup.md environment variables section.

🤖 Prompt for AI Agents
In blt/settings.py around lines 634 to 649 the bounty payout env vars used by
website/views/organization.py are not documented; add BLT_API_TOKEN,
GITHUB_SPONSOR_USERNAME (default DonnieBLT), BLT_ALLOWED_BOUNTY_REPO_1,
BLT_ALLOWED_BOUNTY_REPO_2, and BLT_ALLOWED_BOUNTY_REPO_3 to .env.example with
example values (e.g., BLT_API_TOKEN=changeme, GITHUB_SPONSOR_USERNAME=DonnieBLT,
BLT_ALLOWED_BOUNTY_REPO_1=OWASP-BLT/BLT, BLT_ALLOWED_BOUNTY_REPO_2=org/repo,
BLT_ALLOWED_BOUNTY_REPO_3=org/repo), and update docs/Setup.md in the environment
variables section to document each key, its purpose, required/optional status,
and the default for GITHUB_SPONSOR_USERNAME so deployers know to set them before
enabling bounty payouts.

2 changes: 2 additions & 0 deletions blt/urls.py
Original file line number Diff line number Diff line change
Expand Up @@ -239,6 +239,7 @@
organization_dashboard_hunt_detail,
organization_dashboard_hunt_edit,
organization_hunt_results,
process_bounty_payout,
room_messages_api,
send_message_api,
sizzle,
Expand Down Expand Up @@ -642,6 +643,7 @@
re_path(r"^hunt/$", login_required(HuntCreate.as_view()), name="hunt"),
re_path(r"^bounties/$", Listbounties.as_view(), name="hunts"),
path("bounties/payouts/", BountyPayoutsView.as_view(), name="bounty_payouts"),
path("api/bounty_payout/", csrf_exempt(process_bounty_payout), name="process_bounty_payout"),
path("api/load-more-issues/", load_more_issues, name="load_more_issues"),
re_path(r"^invite/$", InviteCreate.as_view(template_name="invite.html"), name="invite"),
re_path(r"^terms/$", TemplateView.as_view(template_name="terms.html"), name="terms"),
Expand Down
6 changes: 6 additions & 0 deletions website/admin.py
Original file line number Diff line number Diff line change
Expand Up @@ -582,11 +582,15 @@ class GitHubIssueAdmin(admin.ModelAdmin):
"merged_at",
"updated_at",
"url",
"sponsors_tx_id",
"p2p_amount_usd",
"p2p_amount_bch",
"sent_by_user",
"p2p_payment_created_at",
"bch_tx_id",
"sponsors_cancellation_failed",
"sponsors_cancellation_attempts",
"sponsors_cancellation_last_attempt",
)
list_filter = [
"type",
Expand All @@ -596,12 +600,14 @@ class GitHubIssueAdmin(admin.ModelAdmin):
"contributor",
"sent_by_user",
"repo",
"sponsors_cancellation_failed",
]
search_fields = [
"title",
"url",
"user_profile__user__username",
"contributor__name",
"sponsors_tx_id",
"bch_tx_id",
]
date_hierarchy = "created_at"
Expand Down
24 changes: 24 additions & 0 deletions website/migrations/0247_userprofile_preferred_payment_method.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Generated migration for preferred_payment_method field

from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0246_add_user_progress_models"),
]

operations = [
migrations.AddField(
model_name="userprofile",
name="preferred_payment_method",
field=models.CharField(
blank=True,
choices=[("sponsors", "GitHub Sponsors"), ("bch", "Bitcoin Cash")],
default="sponsors",
help_text="Preferred payment method for bounty payouts",
max_length=20,
null=True,
),
),
]
25 changes: 25 additions & 0 deletions website/migrations/0248_githubissue_sponsors_cancel_flags.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("website", "0247_userprofile_preferred_payment_method"),
]

operations = [
migrations.AddField(
model_name="githubissue",
name="sponsors_cancellation_failed",
field=models.BooleanField(default=False),
),
migrations.AddField(
model_name="githubissue",
name="sponsors_cancellation_attempts",
field=models.IntegerField(default=0),
),
migrations.AddField(
model_name="githubissue",
name="sponsors_cancellation_last_attempt",
field=models.DateTimeField(null=True, blank=True),
),
]
12 changes: 12 additions & 0 deletions website/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -731,6 +731,14 @@ class UserProfile(models.Model):
btc_address = models.CharField(max_length=100, blank=True, null=True, validators=[validate_btc_address])
bch_address = models.CharField(max_length=100, blank=True, null=True, validators=[validate_bch_address])
eth_address = models.CharField(max_length=100, blank=True, null=True)
preferred_payment_method = models.CharField(
max_length=20,
choices=[("sponsors", "GitHub Sponsors"), ("bch", "Bitcoin Cash")],
default="sponsors",
blank=True,
null=True,
help_text="Preferred payment method for bounty payouts",
)
created = models.DateTimeField(auto_now_add=True)
tags = models.ManyToManyField(Tag, blank=True)
x_username = models.CharField(max_length=50, blank=True, null=True)
Expand Down Expand Up @@ -1697,6 +1705,10 @@ class GitHubIssue(models.Model):
related_name="github_issue_p2p_payments",
)
bch_tx_id = models.CharField(max_length=255, null=True, blank=True)
# Sponsors cancellation tracking (observability for immediate-cancel flow)
sponsors_cancellation_failed = models.BooleanField(default=False)
sponsors_cancellation_attempts = models.IntegerField(default=0)
sponsors_cancellation_last_attempt = models.DateTimeField(null=True, blank=True)
# Related pull requests
linked_pull_requests = models.ManyToManyField(
"self",
Expand Down
Loading