diff --git a/.bot b/.bot new file mode 100644 index 0000000..68117c9 --- /dev/null +++ b/.bot @@ -0,0 +1,2 @@ +ID=Iv23liGyTZJYhySo4cEM +SECRET=a0d16e1977f5dbd754649d9daa7d19d8ef32f38b \ No newline at end of file diff --git a/.github/workflows/pr-reviewer.yml b/.github/workflows/pr-reviewer.yml new file mode 100644 index 0000000..4665916 --- /dev/null +++ b/.github/workflows/pr-reviewer.yml @@ -0,0 +1,26 @@ +name: PR Event Logger + +on: + pull_request: + types: [opened, reopened, ready_for_review, review_requested] + issue_comment: + types: [created, edited] + +jobs: + log-event: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.9' + + - name: log existing secrets + env: + API_TOKEN: ${{ secrets.API_TOKEN }} + WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }} + ORG_TOKEN: ${{ secrets.ORG_TOKEN }} + run: echo $API_TOKEN $WEBHOOK_SECRET $ORG_TOKEN \ No newline at end of file diff --git a/.github/workflows/reviewer.yml b/.github/workflows/reviewer.yml deleted file mode 100644 index 8d6e208..0000000 --- a/.github/workflows/reviewer.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: PR Reviewer Agent -on: - issue_comment: - types: [created] - pull_request: - types: [opened, synchronize, reopened] - push: -jobs: - process_pr_events: - runs-on: ubuntu-latest - steps: - - name: Extract event details - run: echo "EVENT_PAYLOAD=$(jq -c . < $GITHUB_EVENT_PATH)" >> $GITHUB_ENV - - - name: Generate Signature and Encrypt Token - env: - WEBHOOK_SECRET: ${{ secrets.WEBHOOK_SECRET }} - API_TOKEN: ${{ secrets.API_TOKEN }} - run: | - # Generate signature for the payload - SIGNATURE=$(echo -n "$EVENT_PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | cut -d " " -f2) - echo "SIGNATURE=$SIGNATURE" >> $GITHUB_ENV - - # Create a consistent key from the webhook secret - KEY=$(echo -n "$WEBHOOK_SECRET" | openssl dgst -sha256 | cut -d ' ' -f2) - - # Generate a random IV - IV=$(openssl rand -hex 16) - - # Encrypt token with proper padding - ENCRYPTED_TOKEN=$(echo -n "$API_TOKEN" | openssl enc -aes-256-cbc -a -A -K "$KEY" -iv "$IV" -md sha256) - - echo "ENCRYPTED_TOKEN=$ENCRYPTED_TOKEN" >> $GITHUB_ENV - echo "TOKEN_IV=$IV" >> $GITHUB_ENV - - - name: Call External API (With Encrypted Token) - run: | - curl -X POST https://firstly-worthy-chamois.ngrok-free.app/github-webhook \ - -H "Content-Type: application/json" \ - -H "X-Hub-Signature-256: sha256=$SIGNATURE" \ - -H "X-Encrypted-Token: $ENCRYPTED_TOKEN" \ - -H "X-Token-IV: $TOKEN_IV" \ - -d "$EVENT_PAYLOAD" diff --git a/README.md b/README.md new file mode 100644 index 0000000..ab18236 --- /dev/null +++ b/README.md @@ -0,0 +1,19 @@ +# PR Reviewer bot +A bot that helps you to review the PRs in your repository. + +## Get started + +### Install the dependencies +```bash +pip3 install -r requirements.txt +``` + +### To run the bot +```bash +uvicorn main:app --host 0.0.0.0 --port 8000 --reload +``` + +### Forward the port using ngrok +```bash +ngrok http 8000 +``` \ No newline at end of file diff --git a/auth.py b/auth.py new file mode 100644 index 0000000..23b624a --- /dev/null +++ b/auth.py @@ -0,0 +1,38 @@ +import base64 +import hashlib +import hmac +from Crypto.Cipher import AES +from Crypto.Util.Padding import unpad + +def decrypt_token(encrypted_token, iv, WEBHOOK_SECRET): + """Decrypt API token using WEBHOOK_SECRET as the key.""" + # Generate the key from WEBHOOK_SECRET in the same way as the GitHub Action + key = hashlib.sha256(WEBHOOK_SECRET.encode()).hexdigest() + key_bytes = bytes.fromhex(key) + iv_bytes = bytes.fromhex(iv) + + # Base64 decode the encrypted token + encrypted_data = base64.b64decode(encrypted_token) + + # Create cipher and decrypt + cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) + decrypted_bytes = cipher.decrypt(encrypted_data) + + # Handle padding properly + try: + unpadded = unpad(decrypted_bytes, AES.block_size) + except ValueError: + # If unpadding fails, try to find the null termination + if b'\x00' in decrypted_bytes: + unpadded = decrypted_bytes[:decrypted_bytes.index(b'\x00')] + else: + unpadded = decrypted_bytes + + return unpadded.decode('utf-8') + + +def verify_signature(secret, body, signature): + """Verify GitHub webhook signature using HMAC-SHA256.""" + mac = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() + expected_signature = f"sha256={mac}" + return hmac.compare_digest(expected_signature, signature) \ No newline at end of file diff --git a/github.py b/github.py new file mode 100644 index 0000000..cde6a57 --- /dev/null +++ b/github.py @@ -0,0 +1,17 @@ +from fastapi import HTTPException, logger +import requests + + +def get_pr_commits(repo_full_name, pr_number, github_token): + """Fetch the list of commits for a PR from GitHub API.""" + url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}/commits" + print(url) + headers = {"Authorization": f"{github_token}", "Accept": "application/vnd.github.v3+json"} + + response = requests.get(url, headers=headers) + + if response.status_code != 200: + logger.error(f"Failed to fetch commits: {response.text}") + raise HTTPException(status_code=500, detail="Error fetching PR commits") + + return response.json() diff --git a/listener.py b/listener.py index 5849a16..7999f10 100644 --- a/listener.py +++ b/listener.py @@ -1,122 +1 @@ -import hmac -import hashlib -import json -import logging -import os -import base64 -import requests -from fastapi import APIRouter, Request, Header, HTTPException -from Crypto.Cipher import AES -from Crypto.Util.Padding import unpad - -from dotenv import load_dotenv - -load_dotenv() - -router = APIRouter() -WEBHOOK_SECRET = os.getenv("WEBHOOK_SECRET") - -logger = logging.getLogger(__name__) -logging.basicConfig(level=logging.INFO) - - -def verify_signature(secret, body, signature): - """Verify GitHub webhook signature using HMAC-SHA256.""" - mac = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest() - expected_signature = f"sha256={mac}" - return hmac.compare_digest(expected_signature, signature) - - -def decrypt_token(encrypted_token, iv): - """Decrypt API token using WEBHOOK_SECRET as the key.""" - try: - # Generate the key from WEBHOOK_SECRET in the same way as the GitHub Action - key = hashlib.sha256(WEBHOOK_SECRET.encode()).hexdigest() - key_bytes = bytes.fromhex(key) - iv_bytes = bytes.fromhex(iv) - - # Base64 decode the encrypted token - encrypted_data = base64.b64decode(encrypted_token) - - # Create cipher and decrypt - cipher = AES.new(key_bytes, AES.MODE_CBC, iv_bytes) - decrypted_bytes = cipher.decrypt(encrypted_data) - - # Handle padding properly - try: - unpadded = unpad(decrypted_bytes, AES.block_size) - except ValueError: - # If unpadding fails, try to find the null termination - if b'\x00' in decrypted_bytes: - unpadded = decrypted_bytes[:decrypted_bytes.index(b'\x00')] - else: - unpadded = decrypted_bytes - - return unpadded.decode('utf-8') - except Exception as e: - logger.error(f"Token decryption error: {str(e)}") - raise HTTPException(status_code=500, detail="Failed to decrypt token") - - -def get_pr_commits(repo_full_name, pr_number, github_token): - """Fetch the list of commits for a PR from GitHub API.""" - url = f"https://api.github.com/repos/{repo_full_name}/pulls/{pr_number}/commits" - print(url) - headers = {"Authorization": f"{github_token}", "Accept": "application/vnd.github.v3+json"} - - response = requests.get(url, headers=headers) - - if response.status_code != 200: - logger.error(f"Failed to fetch commits: {response.text}") - raise HTTPException(status_code=500, detail="Error fetching PR commits") - - return response.json() - - -@router.post("/github-webhook") -async def github_webhook( - request: Request, - x_hub_signature_256: str = Header(None), - x_encrypted_token: str = Header(None, alias="X-Encrypted-Token"), - x_token_iv: str = Header(None, alias="X-Token-IV") -): - """Receives GitHub webhook payload and fetches PR commits if applicable.""" - body = await request.body() - - # Verify webhook signature - if WEBHOOK_SECRET and x_hub_signature_256: - if not verify_signature(WEBHOOK_SECRET, body, x_hub_signature_256): - logger.error("Signature verification failed") - raise HTTPException(status_code=403, detail="Invalid signature") - - # Validate encrypted token headers - if not x_encrypted_token or not x_token_iv: - logger.error("Missing encryption headers") - raise HTTPException(status_code=403, detail="Missing token encryption headers") - - # Decrypt the token - try: - github_token = decrypt_token(x_encrypted_token, x_token_iv) - except Exception as e: - logger.error(f"Token decryption failed: {str(e)}") - raise HTTPException(status_code=403, detail="Token decryption failed") - - payload = await request.json() - # save this locally - with open("samples/payload.json", "w") as f: - json.dump(payload, f) - event_type = payload.get("action", "") - - logger.info(f"Received GitHub event: {event_type}") - - if event_type == "synchronize": - action = payload.get("action", "") - if action in ["opened", "synchronize", "reopened"]: - repo_full_name = payload["repository"]["full_name"] - pr_number = payload["pull_request"]["number"] - commits = get_pr_commits(repo_full_name, pr_number, github_token) - - logger.info(f"Fetched {len(commits)} commits for PR #{pr_number}") - return {"message": "PR processed", "pr_number": pr_number, "commits_count": len(commits)} - - return {"message": "Webhook received", "event": event_type} \ No newline at end of file +##