Skip to content
Open
Show file tree
Hide file tree
Changes from 13 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
11 changes: 11 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ BLUESKY_PASSWORD='example#123'
GITHUB_TOKEN="abc123"
ORD_SERVER_URL="http://ipv4:port"

# --- Bounty payout configuration ---
# Required for authenticating bounty payout requests from GitHub Actions
BLT_API_TOKEN=your-bounty-api-token-here
# The GitHub account that will sponsor users (defaults to "DonnieBLT")
GITHUB_SPONSOR_USERNAME=DonnieBLT
# Repositories eligible for automated bounty payouts (prevents unauthorized repos from draining budget)
BLT_ALLOWED_BOUNTY_REPO_1=OWASP-BLT/BLT
# Optionally allow more repos:
BLT_ALLOWED_BOUNTY_REPO_2=other-org/other-repo
BLT_ALLOWED_BOUNTY_REPO_3=another-org/another-repo


GITHUB_CLIENT_SECRET=be8599360ca3e1234
GITHUB_CLIENT_ID=Ov23liITA1234
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 }}"
5 changes: 5 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,12 @@ jobs:
- run: poetry install --with dev
- run: poetry run python manage.py collectstatic --noinput
- name: Run tests
# Note: Selenium tests are tagged with @tag('serial') to avoid SQLite concurrency issues
# The test database uses file-based SQLite to prevent "cannot open savepoint" errors
run: poetry run xvfb-run --auto-servernum python manage.py test -v 3 --failfast
- name: Cleanup test database
if: always()
run: rm -f test_db.sqlite3

docker-test:
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ staticfiles
/env
/pvenv
db.sqlite3
test_db.sqlite3
.idea
/media
.vagrant
Expand Down
45 changes: 39 additions & 6 deletions blt/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -330,17 +330,34 @@
# But make sure we keep the EMAIL_BACKEND setting from above
pass

DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
# Database configuration
# Use file-based SQLite for testing to avoid transaction/savepoint issues
if TESTING:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "test_db.sqlite3"),
"OPTIONS": {
"timeout": 30, # Increase timeout for concurrent access
},
"TEST": {
"NAME": os.path.join(BASE_DIR, "test_db.sqlite3"),
},
}
}
else:
DATABASES = {
"default": {
"ENGINE": "django.db.backends.sqlite3",
"NAME": os.path.join(BASE_DIR, "db.sqlite3"),
}
}
}

if not db_from_env:
print("no database url detected in settings, using sqlite")
else:
DATABASES["default"] = dj_database_url.config(conn_max_age=0, ssl_require=False)
if not TESTING:
DATABASES["default"] = dj_database_url.config(conn_max_age=0, ssl_require=False)

ACCOUNT_EMAIL_REQUIRED = True
ACCOUNT_USERNAME_REQUIRED = True
Expand Down Expand Up @@ -631,3 +648,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
24 changes: 22 additions & 2 deletions docs/Setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,37 @@ cd BLT
cp .env.example .env
```
- Modify the .env file as per your local setup.

## Step 2: Choose your setup method (Docker recommended)
#### Prerequisites for Docker method
Ensure the following are installed on your system before proceeding:

- Docker
- Docker Compose
- PostgreSQL client (optional, for manual database interaction)

---
---

## Bounty Payout Automation Environment Variables

To enable secure and automated bounty payouts via GitHub Sponsors, set the following environment variables in your `.env` file:

- `BLT_API_TOKEN`: Secret token for authenticating payout requests from GitHub Actions. Must match the token used in your workflow.
- `GITHUB_SPONSOR_USERNAME`: The GitHub account that will sponsor users (default: `DonnieBLT`).
- `BLT_ALLOWED_BOUNTY_REPO_1`, `BLT_ALLOWED_BOUNTY_REPO_2`, `BLT_ALLOWED_BOUNTY_REPO_3`: List of repositories eligible for bounty payouts. Only issues from these repos will be processed for payouts. This is a critical security measure to prevent unauthorized repositories from draining the bounty budget.

Example:

```env
BLT_API_TOKEN=your-bounty-api-token-here
GITHUB_SPONSOR_USERNAME=DonnieBLT
BLT_ALLOWED_BOUNTY_REPO_1=OWASP-BLT/BLT
BLT_ALLOWED_BOUNTY_REPO_2=other-org/other-repo
BLT_ALLOWED_BOUNTY_REPO_3=another-org/another-repo
```

**Important:**
- Never share your `BLT_API_TOKEN` publicly.
- Always keep the allowed repo list up to date to ensure only trusted repositories are eligible for payouts.
### 1. Ensure LF Line Endings
Before building the Docker images, ensure all files, especially scripts like `entrypoint.sh`, `.env`, `docker-compose.yml`, `Dockerfile`, `settings.py` use LF line endings. Using CRLF can cause build failures. To verify and correct line endings:

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