From 025bfa3a7aa662fdae4a876613b225d61b709248 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 11:51:14 +0000 Subject: [PATCH 1/5] feat: Add interactive submit-project script for custom challenges MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extended challenge.py with submit-project subcommand - Interactive questionnaire following specified requirements - OSS-Fuzz project URL discovery using grep (no yaml import) - Support for diff mode (..) and full mode analysis - Automatic default branch detection - Added Makefile target for easy access - Validates user input and provides helpful feedback 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Riccardo Schirone --- Makefile | 12 ++ orchestrator/scripts/challenge.py | 175 +++++++++++++++++++++++++++++- 2 files changed, 186 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 0d6c6fd7..211c251c 100644 --- a/Makefile +++ b/Makefile @@ -24,6 +24,7 @@ help: @echo "Testing:" @echo " send-integration-task - Run integration-test task" @echo " send-libpng-task - Run libpng task" + @echo " submit-project - Interactive custom challenge submission" @echo "" @echo "Development:" @echo " install-cscope - Install cscope tool" @@ -162,6 +163,17 @@ send-libpng-task: ./orchestrator/scripts/task_crs.sh @pkill -f "kubectl port-forward" || true +submit-project: + @echo "Starting interactive custom challenge submission..." + @if ! kubectl get namespace $${BUTTERCUP_NAMESPACE:-crs} >/dev/null 2>&1; then \ + echo "Error: CRS namespace not found. Deploy first with 'make deploy'."; \ + exit 1; \ + fi + kubectl port-forward -n $${BUTTERCUP_NAMESPACE:-crs} service/buttercup-ui 31323:1323 & + @sleep 3 + cd orchestrator && python3 scripts/challenge.py submit-project + @pkill -f "kubectl port-forward" || true + # Development targets lint: @echo "Linting all Python code..." diff --git a/orchestrator/scripts/challenge.py b/orchestrator/scripts/challenge.py index 32f889f1..c2f069f4 100755 --- a/orchestrator/scripts/challenge.py +++ b/orchestrator/scripts/challenge.py @@ -2,10 +2,11 @@ import argparse import json +import subprocess import time import urllib.request import sys -from typing import Any +from typing import Any, Optional SECONDS = 1 MINUTES = 60 * SECONDS @@ -548,6 +549,175 @@ def single(challenge_name: str, duration: int) -> None: time.sleep(duration) +def get_project_git_url_from_oss_fuzz(oss_fuzz_url: str, oss_fuzz_ref: str, project_name: str) -> Optional[str]: + """Get project git URL from oss-fuzz project.yaml using grep.""" + try: + # Create a temporary directory and clone/fetch the project.yaml + cmd = [ + "bash", "-c", + f"git clone --depth 1 --branch {oss_fuzz_ref} {oss_fuzz_url} /tmp/oss-fuzz-temp 2>/dev/null && " + f"grep -E '^main_repo:' /tmp/oss-fuzz-temp/projects/{project_name}/project.yaml | " + f"sed 's/main_repo: *//g' | tr -d '\"'" + ] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0 and result.stdout.strip(): + return result.stdout.strip() + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + pass + finally: + # Clean up + subprocess.run(["rm", "-rf", "/tmp/oss-fuzz-temp"], capture_output=True) + return None + + +def get_default_branch(repo_url: str) -> str: + """Get the default branch name for a repository.""" + try: + cmd = ["git", "ls-remote", "--symref", repo_url, "HEAD"] + result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) + if result.returncode == 0: + for line in result.stdout.split('\n'): + if line.startswith('ref: refs/heads/'): + branch_name = line.split('/')[-1].strip() + # Remove any trailing whitespace or tab characters + branch_name = branch_name.split()[0] if branch_name else "main" + return branch_name + except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + pass + return "main" + + +def yes_no_prompt(question: str, default: bool = True) -> bool: + """Ask a yes/no question with a default answer.""" + default_str = "Y/n" if default else "y/N" + while True: + response = input(f"{question} [{default_str}]: ").strip().lower() + if response == "": + return default + elif response in ["y", "yes"]: + return True + elif response in ["n", "no"]: + return False + else: + print("Please answer 'y' or 'n'") + + +def submit_project() -> None: + """Interactive script to submit a custom challenge project.""" + print("=== Buttercup Custom Challenge Submission ===\n") + + task_data = {} + + # Question 1: OSS-Fuzz usage + use_upstream_oss_fuzz = yes_no_prompt("Do you want to use upstream google/oss-fuzz?", True) + + if use_upstream_oss_fuzz: + task_data["fuzz_tooling_url"] = "https://github.com/google/oss-fuzz" + task_data["fuzz_tooling_ref"] = "master" + else: + custom_oss_fuzz_url = input("Enter custom oss-fuzz URL: ").strip() + custom_oss_fuzz_ref = input("Enter custom oss-fuzz ref [master]: ").strip() or "master" + task_data["fuzz_tooling_url"] = custom_oss_fuzz_url + task_data["fuzz_tooling_ref"] = custom_oss_fuzz_ref + + # Question 2: OSS-Fuzz project name + project_name = input("What's the oss-fuzz project name? ").strip() + if not project_name: + print("Error: Project name is required") + return + task_data["fuzz_tooling_project_name"] = project_name + + # Determine project URL from oss-fuzz + print(f"\nLooking up project URL from oss-fuzz...") + project_url = get_project_git_url_from_oss_fuzz( + task_data["fuzz_tooling_url"], + task_data["fuzz_tooling_ref"], + project_name + ) + + # Question 3: Confirm or provide project URL + if project_url: + print(f"Determined project URL: {project_url}") + if not yes_no_prompt("Is this URL correct?", True): + project_url = input("Please provide the correct project URL: ").strip() + else: + print("Could not determine project URL from oss-fuzz") + project_url = input("Please provide the project URL: ").strip() + + if not project_url: + print("Error: Project URL is required") + return + task_data["challenge_repo_url"] = project_url + + # Question 4: Specific branch/commit analysis + analyze_specific = yes_no_prompt("Do you want to analyze a specific branch/commit?", False) + + if analyze_specific: + branch_or_commit = input("Enter branch/commit (use '..' for diff mode, e.g., 'base..head'): ").strip() + if not branch_or_commit: + print("Error: Branch/commit is required") + return + + if ".." in branch_or_commit: + # Diff mode + base_ref, head_ref = branch_or_commit.split("..", 1) + task_data["challenge_repo_base_ref"] = base_ref.strip() + task_data["challenge_repo_head_ref"] = head_ref.strip() + print(f"Using diff mode: {base_ref} -> {head_ref}") + else: + # Full mode + task_data["challenge_repo_head_ref"] = branch_or_commit + print(f"Using full mode: analyzing {branch_or_commit}") + else: + # Use default branch + default_branch = get_default_branch(project_url) + task_data["challenge_repo_head_ref"] = default_branch + print(f"Using default branch: {default_branch}") + + # Question 5: Analysis duration + while True: + try: + duration_minutes = input("How long should the analysis last? (in minutes) [30]: ").strip() + if not duration_minutes: + duration_minutes = "30" + duration_seconds = int(duration_minutes) * MINUTES + task_data["duration"] = duration_seconds + break + except ValueError: + print("Please enter a valid number of minutes") + + # Set default values + task_data["harnesses_included"] = True + + # Display final configuration + print("\n=== Final Configuration ===") + print(json.dumps(task_data, indent=2)) + + if not yes_no_prompt("\nSubmit this configuration?", True): + print("Cancelled.") + return + + # Submit the task + try: + url = "http://127.0.0.1:31323/webhook/trigger_task" + json_bytes = json.dumps(task_data).encode("utf-8") + + req = urllib.request.Request(url, data=json_bytes, headers={"Content-Type": "application/json"}) + print("\n=== Submitting Request ===") + print_request(req) + + with urllib.request.urlopen(req) as response: + response_data = response.read().decode("utf-8") + print(f"Response status: {response.status}") + if response.status == 200: + print("✓ Task submitted successfully!") + else: + print(f"✗ Error response: {response_data}") + + except Exception as e: + print(f"✗ Error submitting task: {e}") + + def main() -> None: """Main function.""" epilog_text = """ @@ -598,6 +768,9 @@ def main() -> None: single_parser.add_argument("duration", type=int, help="Duration in seconds") single_parser.set_defaults(func=lambda args: single(args.name, args.duration)) + submit_project_parser = subparsers.add_parser("submit-project", help="Interactive submission of custom challenge project") + submit_project_parser.set_defaults(func=lambda args: submit_project()) + args = parser.parse_args() if hasattr(args, "func"): args.func(args) From 55eaf2f7f3d20c817889866816dc14aaf60d8dd4 Mon Sep 17 00:00:00 2001 From: "claude[bot]" <209825114+claude[bot]@users.noreply.github.com> Date: Mon, 4 Aug 2025 12:35:58 +0000 Subject: [PATCH 2/5] Replace grep/sed with Python parsing in submit-project script - Replace shell commands in get_project_git_url_from_oss_fuzz() with pure Python - Use tempfile.mkdtemp() for proper temporary directory management - Parse project.yaml line by line with regex for main_repo extraction - Handle both quoted and unquoted YAML values - Improve error handling and resource cleanup Co-authored-by: Riccardo Schirone --- orchestrator/scripts/challenge.py | 48 ++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 13 deletions(-) diff --git a/orchestrator/scripts/challenge.py b/orchestrator/scripts/challenge.py index c2f069f4..d997852c 100755 --- a/orchestrator/scripts/challenge.py +++ b/orchestrator/scripts/challenge.py @@ -2,7 +2,10 @@ import argparse import json +import os +import re import subprocess +import tempfile import time import urllib.request import sys @@ -550,23 +553,42 @@ def single(challenge_name: str, duration: int) -> None: def get_project_git_url_from_oss_fuzz(oss_fuzz_url: str, oss_fuzz_ref: str, project_name: str) -> Optional[str]: - """Get project git URL from oss-fuzz project.yaml using grep.""" + """Get project git URL from oss-fuzz project.yaml.""" + temp_dir = None try: - # Create a temporary directory and clone/fetch the project.yaml - cmd = [ - "bash", "-c", - f"git clone --depth 1 --branch {oss_fuzz_ref} {oss_fuzz_url} /tmp/oss-fuzz-temp 2>/dev/null && " - f"grep -E '^main_repo:' /tmp/oss-fuzz-temp/projects/{project_name}/project.yaml | " - f"sed 's/main_repo: *//g' | tr -d '\"'" + # Create a temporary directory and clone the oss-fuzz repository + temp_dir = tempfile.mkdtemp(prefix="oss-fuzz-") + + # Clone the repository + clone_cmd = [ + "git", "clone", "--depth", "1", "--branch", oss_fuzz_ref, + oss_fuzz_url, temp_dir ] - result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) - if result.returncode == 0 and result.stdout.strip(): - return result.stdout.strip() - except (subprocess.TimeoutExpired, subprocess.CalledProcessError): + result = subprocess.run(clone_cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + return None + + # Read and parse the project.yaml file + project_yaml_path = os.path.join(temp_dir, "projects", project_name, "project.yaml") + if not os.path.exists(project_yaml_path): + return None + + with open(project_yaml_path, 'r', encoding='utf-8') as f: + for line in f: + line = line.strip() + # Look for lines starting with 'main_repo:' + if line.startswith('main_repo:'): + # Extract the URL after 'main_repo:' + main_repo_match = re.match(r'^main_repo:\s*["\']?([^"\']+)["\']?', line) + if main_repo_match: + return main_repo_match.group(1).strip() + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, IOError, OSError): pass finally: - # Clean up - subprocess.run(["rm", "-rf", "/tmp/oss-fuzz-temp"], capture_output=True) + # Clean up temporary directory + if temp_dir and os.path.exists(temp_dir): + subprocess.run(["rm", "-rf", temp_dir], capture_output=True) return None From e1c34cb2fe622099ac45782320da9bfd321898d9 Mon Sep 17 00:00:00 2001 From: Riccardo Schirone Date: Mon, 4 Aug 2025 13:30:18 +0000 Subject: [PATCH 3/5] fix lint --- orchestrator/scripts/challenge.py | 85 +++++++++++++++---------------- 1 file changed, 42 insertions(+), 43 deletions(-) diff --git a/orchestrator/scripts/challenge.py b/orchestrator/scripts/challenge.py index d997852c..f2dc6172 100755 --- a/orchestrator/scripts/challenge.py +++ b/orchestrator/scripts/challenge.py @@ -9,7 +9,7 @@ import time import urllib.request import sys -from typing import Any, Optional +from typing import Any, Optional, TextIO SECONDS = 1 MINUTES = 60 * SECONDS @@ -319,7 +319,7 @@ } -def print_request(req, file=sys.stderr): +def print_request(req: urllib.request.Request, file: TextIO = sys.stderr) -> None: """Print a request object.""" print(f"{req.get_method()} {req.full_url}", file=file) @@ -337,9 +337,9 @@ def print_request(req, file=sys.stderr): print(json.dumps(data_json, indent=2), file=file) except (json.JSONDecodeError, UnicodeDecodeError): - print(f"{req.data}", file=file) + print(f"{req.data!r}", file=file) else: - print(f"{req.data}", file=file) + print(f"{req.data!r}", file=file) print("\n", file=file) @@ -558,31 +558,28 @@ def get_project_git_url_from_oss_fuzz(oss_fuzz_url: str, oss_fuzz_ref: str, proj try: # Create a temporary directory and clone the oss-fuzz repository temp_dir = tempfile.mkdtemp(prefix="oss-fuzz-") - + # Clone the repository - clone_cmd = [ - "git", "clone", "--depth", "1", "--branch", oss_fuzz_ref, - oss_fuzz_url, temp_dir - ] + clone_cmd = ["git", "clone", "--depth", "1", "--branch", oss_fuzz_ref, oss_fuzz_url, temp_dir] result = subprocess.run(clone_cmd, capture_output=True, text=True, timeout=30) if result.returncode != 0: return None - + # Read and parse the project.yaml file project_yaml_path = os.path.join(temp_dir, "projects", project_name, "project.yaml") if not os.path.exists(project_yaml_path): return None - - with open(project_yaml_path, 'r', encoding='utf-8') as f: + + with open(project_yaml_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() # Look for lines starting with 'main_repo:' - if line.startswith('main_repo:'): + if line.startswith("main_repo:"): # Extract the URL after 'main_repo:' main_repo_match = re.match(r'^main_repo:\s*["\']?([^"\']+)["\']?', line) if main_repo_match: return main_repo_match.group(1).strip() - + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, IOError, OSError): pass finally: @@ -598,9 +595,9 @@ def get_default_branch(repo_url: str) -> str: cmd = ["git", "ls-remote", "--symref", repo_url, "HEAD"] result = subprocess.run(cmd, capture_output=True, text=True, timeout=30) if result.returncode == 0: - for line in result.stdout.split('\n'): - if line.startswith('ref: refs/heads/'): - branch_name = line.split('/')[-1].strip() + for line in result.stdout.split("\n"): + if line.startswith("ref: refs/heads/"): + branch_name = line.split("/")[-1].strip() # Remove any trailing whitespace or tab characters branch_name = branch_name.split()[0] if branch_name else "main" return branch_name @@ -627,12 +624,12 @@ def yes_no_prompt(question: str, default: bool = True) -> bool: def submit_project() -> None: """Interactive script to submit a custom challenge project.""" print("=== Buttercup Custom Challenge Submission ===\n") - - task_data = {} - + + task_data: dict[str, str | int | bool] = {} + # Question 1: OSS-Fuzz usage use_upstream_oss_fuzz = yes_no_prompt("Do you want to use upstream google/oss-fuzz?", True) - + if use_upstream_oss_fuzz: task_data["fuzz_tooling_url"] = "https://github.com/google/oss-fuzz" task_data["fuzz_tooling_ref"] = "master" @@ -641,22 +638,22 @@ def submit_project() -> None: custom_oss_fuzz_ref = input("Enter custom oss-fuzz ref [master]: ").strip() or "master" task_data["fuzz_tooling_url"] = custom_oss_fuzz_url task_data["fuzz_tooling_ref"] = custom_oss_fuzz_ref - + # Question 2: OSS-Fuzz project name project_name = input("What's the oss-fuzz project name? ").strip() if not project_name: print("Error: Project name is required") return task_data["fuzz_tooling_project_name"] = project_name - + # Determine project URL from oss-fuzz - print(f"\nLooking up project URL from oss-fuzz...") - project_url = get_project_git_url_from_oss_fuzz( - task_data["fuzz_tooling_url"], - task_data["fuzz_tooling_ref"], - project_name - ) - + print("\nLooking up project URL from oss-fuzz...") + oss_fuzz_url = task_data["fuzz_tooling_url"] + oss_fuzz_ref = task_data["fuzz_tooling_ref"] + assert isinstance(oss_fuzz_url, str) + assert isinstance(oss_fuzz_ref, str) + project_url = get_project_git_url_from_oss_fuzz(oss_fuzz_url, oss_fuzz_ref, project_name) + # Question 3: Confirm or provide project URL if project_url: print(f"Determined project URL: {project_url}") @@ -665,21 +662,21 @@ def submit_project() -> None: else: print("Could not determine project URL from oss-fuzz") project_url = input("Please provide the project URL: ").strip() - + if not project_url: print("Error: Project URL is required") return task_data["challenge_repo_url"] = project_url - + # Question 4: Specific branch/commit analysis analyze_specific = yes_no_prompt("Do you want to analyze a specific branch/commit?", False) - + if analyze_specific: branch_or_commit = input("Enter branch/commit (use '..' for diff mode, e.g., 'base..head'): ").strip() if not branch_or_commit: print("Error: Branch/commit is required") return - + if ".." in branch_or_commit: # Diff mode base_ref, head_ref = branch_or_commit.split("..", 1) @@ -695,7 +692,7 @@ def submit_project() -> None: default_branch = get_default_branch(project_url) task_data["challenge_repo_head_ref"] = default_branch print(f"Using default branch: {default_branch}") - + # Question 5: Analysis duration while True: try: @@ -707,27 +704,27 @@ def submit_project() -> None: break except ValueError: print("Please enter a valid number of minutes") - + # Set default values task_data["harnesses_included"] = True - + # Display final configuration print("\n=== Final Configuration ===") print(json.dumps(task_data, indent=2)) - + if not yes_no_prompt("\nSubmit this configuration?", True): print("Cancelled.") return - + # Submit the task try: url = "http://127.0.0.1:31323/webhook/trigger_task" json_bytes = json.dumps(task_data).encode("utf-8") - + req = urllib.request.Request(url, data=json_bytes, headers={"Content-Type": "application/json"}) print("\n=== Submitting Request ===") print_request(req) - + with urllib.request.urlopen(req) as response: response_data = response.read().decode("utf-8") print(f"Response status: {response.status}") @@ -735,7 +732,7 @@ def submit_project() -> None: print("✓ Task submitted successfully!") else: print(f"✗ Error response: {response_data}") - + except Exception as e: print(f"✗ Error submitting task: {e}") @@ -790,7 +787,9 @@ def main() -> None: single_parser.add_argument("duration", type=int, help="Duration in seconds") single_parser.set_defaults(func=lambda args: single(args.name, args.duration)) - submit_project_parser = subparsers.add_parser("submit-project", help="Interactive submission of custom challenge project") + submit_project_parser = subparsers.add_parser( + "submit-project", help="Interactive submission of custom challenge project" + ) submit_project_parser.set_defaults(func=lambda args: submit_project()) args = parser.parse_args() From 3fe370300c743fa38605cba4a4f4bec0d2b0d05f Mon Sep 17 00:00:00 2001 From: Riccardo Schirone Date: Mon, 4 Aug 2025 13:37:51 +0000 Subject: [PATCH 4/5] add dry run mode --- orchestrator/scripts/challenge.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/orchestrator/scripts/challenge.py b/orchestrator/scripts/challenge.py index f2dc6172..2479f7a1 100755 --- a/orchestrator/scripts/challenge.py +++ b/orchestrator/scripts/challenge.py @@ -621,7 +621,7 @@ def yes_no_prompt(question: str, default: bool = True) -> bool: print("Please answer 'y' or 'n'") -def submit_project() -> None: +def submit_project(dry_run: bool = False) -> None: """Interactive script to submit a custom challenge project.""" print("=== Buttercup Custom Challenge Submission ===\n") @@ -712,6 +712,20 @@ def submit_project() -> None: print("\n=== Final Configuration ===") print(json.dumps(task_data, indent=2)) + if dry_run: + print("\n=== DRY RUN MODE ===") + print("This is a dry run - no task will be submitted.") + print("The following configuration would be submitted:") + print_request( + urllib.request.Request( + "http://127.0.0.1:31323/webhook/trigger_task", + data=json.dumps(task_data).encode("utf-8"), + headers={"Content-Type": "application/json"}, + ) + ) + print("✓ Dry run completed successfully!") + return + if not yes_no_prompt("\nSubmit this configuration?", True): print("Cancelled.") return @@ -790,7 +804,10 @@ def main() -> None: submit_project_parser = subparsers.add_parser( "submit-project", help="Interactive submission of custom challenge project" ) - submit_project_parser.set_defaults(func=lambda args: submit_project()) + submit_project_parser.add_argument( + "--dry-run", action="store_true", help="Show what would be submitted without actually submitting" + ) + submit_project_parser.set_defaults(func=lambda args: submit_project(dry_run=args.dry_run)) args = parser.parse_args() if hasattr(args, "func"): From 5505f54094564a1dd1c18f7fc035b2f6836a1932 Mon Sep 17 00:00:00 2001 From: Riccardo Schirone Date: Mon, 4 Aug 2025 13:40:51 +0000 Subject: [PATCH 5/5] use TemporaryDirectory --- orchestrator/scripts/challenge.py | 57 ++++++++++++++----------------- 1 file changed, 25 insertions(+), 32 deletions(-) diff --git a/orchestrator/scripts/challenge.py b/orchestrator/scripts/challenge.py index 2479f7a1..5f363007 100755 --- a/orchestrator/scripts/challenge.py +++ b/orchestrator/scripts/challenge.py @@ -554,38 +554,31 @@ def single(challenge_name: str, duration: int) -> None: def get_project_git_url_from_oss_fuzz(oss_fuzz_url: str, oss_fuzz_ref: str, project_name: str) -> Optional[str]: """Get project git URL from oss-fuzz project.yaml.""" - temp_dir = None - try: - # Create a temporary directory and clone the oss-fuzz repository - temp_dir = tempfile.mkdtemp(prefix="oss-fuzz-") - - # Clone the repository - clone_cmd = ["git", "clone", "--depth", "1", "--branch", oss_fuzz_ref, oss_fuzz_url, temp_dir] - result = subprocess.run(clone_cmd, capture_output=True, text=True, timeout=30) - if result.returncode != 0: - return None - - # Read and parse the project.yaml file - project_yaml_path = os.path.join(temp_dir, "projects", project_name, "project.yaml") - if not os.path.exists(project_yaml_path): - return None - - with open(project_yaml_path, "r", encoding="utf-8") as f: - for line in f: - line = line.strip() - # Look for lines starting with 'main_repo:' - if line.startswith("main_repo:"): - # Extract the URL after 'main_repo:' - main_repo_match = re.match(r'^main_repo:\s*["\']?([^"\']+)["\']?', line) - if main_repo_match: - return main_repo_match.group(1).strip() - - except (subprocess.TimeoutExpired, subprocess.CalledProcessError, IOError, OSError): - pass - finally: - # Clean up temporary directory - if temp_dir and os.path.exists(temp_dir): - subprocess.run(["rm", "-rf", temp_dir], capture_output=True) + with tempfile.TemporaryDirectory(prefix="oss-fuzz-") as temp_dir: + try: + # Clone the repository + clone_cmd = ["git", "clone", "--depth", "1", "--branch", oss_fuzz_ref, oss_fuzz_url, temp_dir] + result = subprocess.run(clone_cmd, capture_output=True, text=True, timeout=30) + if result.returncode != 0: + return None + + # Read and parse the project.yaml file + project_yaml_path = os.path.join(temp_dir, "projects", project_name, "project.yaml") + if not os.path.exists(project_yaml_path): + return None + + with open(project_yaml_path, "r", encoding="utf-8") as f: + for line in f: + line = line.strip() + # Look for lines starting with 'main_repo:' + if line.startswith("main_repo:"): + # Extract the URL after 'main_repo:' + main_repo_match = re.match(r'^main_repo:\s*["\']?([^"\']+)["\']?', line) + if main_repo_match: + return main_repo_match.group(1).strip() + + except (subprocess.TimeoutExpired, subprocess.CalledProcessError, IOError, OSError): + pass return None