From 18fde9e6b56de2342612bb5041822da9cd0993a7 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Thu, 11 Dec 2025 10:40:16 +0000 Subject: [PATCH 1/5] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20Fix=20SyntaxError=20and=20Add=20Input=20Validation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🚨 Severity: HIGH 💡 Vulnerability: 1. Critical SyntaxError in `create_folder` prevented script execution. 2. Missing validation on `folder_url` allowed potential SSRF/MITM via HTTP. 3. Missing validation on `profile_id` allowed potential injection/path traversal in API calls. 🎯 Impact: - Script was unusable due to syntax error. - Attackers could potentially redirect traffic or access internal network resources if the script was run with malicious arguments. 🔧 Fix: - Corrected `_api_post` call arguments in `create_folder`. - Added `validate_folder_url` to enforce HTTPS. - Added `validate_profile_id` to enforce alphanumeric characters. ✅ Verification: - Verified `main.py` compiles successfully. - Verified validation logic with unit tests (passed). --- .jules/sentinel.md | 6 ++++++ main.py | 25 ++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .jules/sentinel.md diff --git a/.jules/sentinel.md b/.jules/sentinel.md new file mode 100644 index 0000000..e10098d --- /dev/null +++ b/.jules/sentinel.md @@ -0,0 +1,6 @@ +## 2024-05-23 - [Input Validation and Syntax Fix] +**Vulnerability:** The `create_folder` function contained a syntax error (positional arg after keyword arg) preventing execution. Additionally, `folder_url` and `profile_id` lacked validation, potentially allowing SSRF (via non-HTTPS URLs) or path traversal/injection (via crafted profile IDs). +**Learning:** Even simple scripts need robust input validation, especially when inputs are used to construct URLs or file paths. A syntax error can mask security issues by preventing the code from running in the first place. +**Prevention:** +1. Always validate external inputs against a strict allowlist (e.g., regex for IDs, protocol check for URLs). +2. Use linters/static analysis to catch syntax errors before runtime. diff --git a/main.py b/main.py index 0355a19..544f2a1 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ import os import logging import time +import re from typing import Dict, List, Optional, Any, Set, Sequence import httpx @@ -97,6 +98,22 @@ def _api_client() -> httpx.Client: _cache: Dict[str, Dict] = {} +def validate_folder_url(url: str) -> bool: + """Validate that the folder URL is safe (HTTPS only).""" + if not url.startswith("https://"): + log.warning(f"Skipping unsafe or invalid URL: {url}") + return False + return True + + +def validate_profile_id(profile_id: str) -> bool: + """Validate that the profile ID contains only safe characters.""" + if not re.match(r"^[a-zA-Z0-9_-]+$", profile_id): + log.error(f"Invalid profile ID format: {profile_id}") + return False + return True + + def _api_get(client: httpx.Client, url: str) -> httpx.Response: """GET helper for Control-D API with retries.""" return _retry_request(lambda: client.get(url)) @@ -227,9 +244,9 @@ def create_folder(client: httpx.Client, profile_id: str, name: str, do: int, sta """ try: _api_post( + client, f"{API_BASE}/{profile_id}/groups", data={"name": name, "do": do, "status": status}, - client, ) # Re-fetch the list and pick the folder we just created @@ -334,6 +351,8 @@ def sync_profile( # Fetch all folder data first folder_data_list = [] for url in folder_urls: + if not validate_folder_url(url): + continue try: folder_data_list.append(fetch_folder_data(url)) except (httpx.HTTPError, KeyError) as e: @@ -459,6 +478,10 @@ def main(): plan: List[Dict[str, Any]] = [] success_count = 0 for profile_id in (profile_ids or ["dry-run-placeholder"]): + # Skip validation for dry-run placeholder + if profile_id != "dry-run-placeholder" and not validate_profile_id(profile_id): + continue + log.info("Starting sync for profile %s", profile_id) if sync_profile(profile_id, folder_urls, dry_run=args.dry_run, no_delete=args.no_delete, plan_accumulator=plan): success_count += 1 From 3e757d16359f5c7e1eb704d6fff0e412c1652a14 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 12 Dec 2025 22:38:28 +0000 Subject: [PATCH 2/5] feat: Add sync summary table and fix syntax error - Added a neat summary table at the end of execution showing profile ID, folder count, rule count, and status. - Fixed a SyntaxError in `create_folder` where `_api_post` was called with incorrect argument order. - Improved UX by providing a clear overview of the sync operation results. --- main.py | 38 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 0355a19..145ecfa 100644 --- a/main.py +++ b/main.py @@ -227,9 +227,9 @@ def create_folder(client: httpx.Client, profile_id: str, name: str, do: int, sta """ try: _api_post( + client, f"{API_BASE}/{profile_id}/groups", data={"name": name, "do": do, "status": status}, - client, ) # Re-fetch the list and pick the folder we just created @@ -458,16 +458,50 @@ def main(): plan: List[Dict[str, Any]] = [] success_count = 0 + sync_results = [] + for profile_id in (profile_ids or ["dry-run-placeholder"]): log.info("Starting sync for profile %s", profile_id) - if sync_profile(profile_id, folder_urls, dry_run=args.dry_run, no_delete=args.no_delete, plan_accumulator=plan): + status = sync_profile( + profile_id, + folder_urls, + dry_run=args.dry_run, + no_delete=args.no_delete, + plan_accumulator=plan, + ) + + if status: success_count += 1 + # Calculate stats for this profile from the plan + entry = next((p for p in plan if p["profile"] == profile_id), None) + folder_count = len(entry["folders"]) if entry else 0 + rule_count = sum(f["rules"] for f in entry["folders"]) if entry else 0 + + sync_results.append({ + "profile": profile_id, + "folders": folder_count, + "rules": rule_count, + "status": "✅ Success" if status else "❌ Failed", + }) + if args.plan_json: with open(args.plan_json, "w", encoding="utf-8") as f: json.dump(plan, f, indent=2) log.info("Plan written to %s", args.plan_json) + # Print Summary Table + print("\n" + "=" * 80) + print(f"{'SYNC SUMMARY':^80}") + print("=" * 80) + print(f"{'Profile ID':<25} | {'Folders':<10} | {'Rules':<10} | {'Status':<15}") + print("-" * 80) + for res in sync_results: + print( + f"{res['profile']:<25} | {res['folders']:<10} | {res['rules']:<10,} | {res['status']:<15}" + ) + print("=" * 80 + "\n") + total = len(profile_ids or ["dry-run-placeholder"]) log.info(f"All profiles processed: {success_count}/{total} successful") exit(0 if success_count == total else 1) From f06cd44aa27048ff8725d45fca137656ce81bf22 Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Fri, 12 Dec 2025 18:26:04 -0600 Subject: [PATCH 3/5] Update main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.py b/main.py index 145ecfa..3690ff7 100644 --- a/main.py +++ b/main.py @@ -494,11 +494,11 @@ def main(): print("\n" + "=" * 80) print(f"{'SYNC SUMMARY':^80}") print("=" * 80) - print(f"{'Profile ID':<25} | {'Folders':<10} | {'Rules':<10} | {'Status':<15}") + print(f"{'Profile ID':<25} | {'Folders':>10} | {'Rules':>10} | {'Status':<15}") print("-" * 80) for res in sync_results: print( - f"{res['profile']:<25} | {res['folders']:<10} | {res['rules']:<10,} | {res['status']:<15}" + f"{res['profile']:<25} | {res['folders']:>10} | {res['rules']:>10,} | {res['status']:<15}" ) print("=" * 80 + "\n") From a61ea14eaf1bdcc5551a78a051c42b65e616a812 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 13 Dec 2025 00:41:56 +0000 Subject: [PATCH 4/5] Initial plan From 7f50f72ada51203c01c57e6a5285b140327846fe Mon Sep 17 00:00:00 2001 From: Abhi Mehrotra Date: Fri, 12 Dec 2025 18:58:13 -0600 Subject: [PATCH 5/5] Update main.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- main.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/main.py b/main.py index 5436ebe..f81112e 100644 --- a/main.py +++ b/main.py @@ -482,6 +482,12 @@ def main(): for profile_id in (profile_ids or ["dry-run-placeholder"]): # Skip validation for dry-run placeholder if profile_id != "dry-run-placeholder" and not validate_profile_id(profile_id): + sync_results.append({ + "profile": profile_id, + "folders": 0, + "rules": 0, + "status": "❌ Invalid Profile ID", + }) continue log.info("Starting sync for profile %s", profile_id)