🛡️ Sentinel: [SECURITY] Enforce secure URL validation and file permissions#169
🛡️ Sentinel: [SECURITY] Enforce secure URL validation and file permissions#169
Conversation
…sions Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com>
|
👋 Jules, reporting for duty! I'm here to lend a hand with this pull request. When you start a review, I'll add a 👀 emoji to each comment to let you know I've read it. I'll focus on feedback directed at me and will do my best to stay out of conversations between you and other bots or reviewers to keep the noise down. I'll push a commit with your requested changes shortly after. Please note there might be a delay between these steps, but rest assured I'm on the job! For more direct control, you can switch me to Reactive Mode. When this mode is on, I will only act on comments where you specifically mention me with New to Jules? Learn more at jules.google/docs. For security, I will only act on instructions from the user who triggered this task. |
|
Merging to
|
Summary of ChangesHello @abhimehro, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request significantly strengthens the security posture of the application by implementing robust checks for URL validation and secure file handling. It prevents the inadvertent exposure of credentials through URLs and ensures that sensitive generated files are protected from unauthorized access, thereby enhancing the overall integrity and confidentiality of the system's operations. Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
| mock_dns.return_value = [(2, 1, 6, "", ("1.1.1.1", 443))] | ||
| # URL with credentials | ||
| url = "https://user:pass@example.com/list.json" | ||
| assert m.validate_folder_url(url) is False |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # URL with credentials | ||
| url = "https://user:pass@example.com/list.json" | ||
| assert m.validate_folder_url(url) is False | ||
| assert mock_log.warning.called |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # Check that warning message contains "credentials detected" | ||
| # call_args is (args, kwargs) | ||
| args, _ = mock_log.warning.call_args | ||
| assert "credentials detected" in args[0] |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| mock_args.no_delete = False | ||
|
|
||
| monkeypatch.setattr(m, "parse_args", lambda: mock_args) | ||
| m.TOKEN = "dummy" |
Check notice
Code scanning / Bandit
Possible hardcoded password: 'dummy' Note test
| # Run main, expect SystemExit(0) on success | ||
| with pytest.raises(SystemExit) as e: | ||
| m.main() | ||
| assert e.value.code == 0 |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert e.value.code == 0 | ||
|
|
||
| # Verify file exists | ||
| assert plan_file.exists() |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) | ||
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) | ||
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | ||
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| # S_IRWXO = 0o007 (other rwx) | ||
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | ||
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" | ||
| assert mode & stat.S_IRUSR, "Owner should have read" |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | ||
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" | ||
| assert mode & stat.S_IRUSR, "Owner should have read" | ||
| assert mode & stat.S_IWUSR, "Owner should have write" |
Check notice
Code scanning / Bandit
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
main.py
Outdated
Check warning
Code scanning / Prospector (reported by Codacy)
Use lazy % formatting in logging functions (logging-fstring-interpolation) Warning
|
|
||
|
|
||
| # Case 15: plan.json is created with secure permissions | ||
| def test_plan_json_secure_permissions(monkeypatch, tmp_path, capsys): |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'capsys' (unused-argument) Warning test
| monkeypatch.setattr(m, "warm_up_cache", MagicMock()) | ||
|
|
||
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'no_delete' (unused-argument) Warning test
| monkeypatch.setattr(m, "warm_up_cache", MagicMock()) | ||
|
|
||
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'urls' (unused-argument) Warning test
| monkeypatch.setattr(m, "warm_up_cache", MagicMock()) | ||
|
|
||
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): |
Check warning
Code scanning / Prospector (reported by Codacy)
Unused argument 'dry_run' (unused-argument) Warning test
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | ||
|
|
||
| # Print Summary Table | ||
| # Determine the width for the Profile ID column (min 25) |
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "fd" doesn't conform to snake_case naming style Warning
main.py
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "f" doesn't conform to snake_case naming style Warning
main.py
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
|
|
||
| # Case 15: plan.json is created with secure permissions | ||
| def test_plan_json_secure_permissions(monkeypatch, tmp_path, capsys): | ||
| # Prepare environment |
Check warning
Code scanning / Pylint (reported by Codacy)
Missing function docstring Warning test
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): | ||
| if plan_accumulator is not None: | ||
| # Add a dummy plan entry |
Check warning
Code scanning / Pylint (reported by Codacy)
Missing function docstring Warning test
test_main.py
Outdated
Check warning
Code scanning / Pylint (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning test
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): | ||
| if plan_accumulator is not None: | ||
| # Add a dummy plan entry |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'capsys' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'urls' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'dry_run' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylint (reported by Codacy)
Unused argument 'no_delete' Note test
| mock_args.folder_url = [] | ||
| mock_args.no_delete = False | ||
|
|
||
| monkeypatch.setattr(m, "parse_args", lambda: mock_args) |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| mock_args.no_delete = False | ||
|
|
||
| monkeypatch.setattr(m, "parse_args", lambda: mock_args) | ||
| m.TOKEN = "dummy" |
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
|
|
||
| # Mock warm_up_cache to avoid actual network | ||
| monkeypatch.setattr(m, "warm_up_cache", MagicMock()) | ||
|
|
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
test_main.py
Outdated
Check notice
Code scanning / Bandit (reported by Codacy)
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | ||
|
|
||
| # Print Summary Table | ||
| # Determine the width for the Profile ID column (min 25) |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "fd" doesn't conform to snake_case naming style Warning
main.py
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "f" doesn't conform to snake_case naming style Warning
main.py
Outdated
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "e" doesn't conform to snake_case naming style Warning
|
|
||
| # Case 15: plan.json is created with secure permissions | ||
| def test_plan_json_secure_permissions(monkeypatch, tmp_path, capsys): | ||
| # Prepare environment |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Missing function or method docstring Warning test
| # Case 15: plan.json is created with secure permissions | ||
| def test_plan_json_secure_permissions(monkeypatch, tmp_path, capsys): | ||
| # Prepare environment | ||
| monkeypatch.setenv("TOKEN", "dummy") |
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Variable name "m" doesn't conform to snake_case naming style Warning test
main.py
Outdated
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Use lazy % formatting in logging functions Note
| # Mock sync_profile so it returns True and populates plan | ||
| def mock_sync(pid, urls, dry_run, no_delete, plan_accumulator=None): | ||
| if plan_accumulator is not None: | ||
| # Add a dummy plan entry |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'capsys' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'no_delete' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'urls' Note test
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) |
Check notice
Code scanning / Pylintpython3 (reported by Codacy)
Unused argument 'dry_run' Note test
There was a problem hiding this comment.
Code Review
This pull request introduces two valuable security enhancements: rejecting URLs with embedded credentials to prevent accidental leakage, and creating the plan.json file with secure 0o600 permissions to prevent unauthorized access. The changes are well-implemented and accompanied by new tests. My feedback includes a suggestion to make the permission validation in the new test more precise and robust.
| mode = os.stat(plan_file).st_mode | ||
| # Check permissions are 600 (rw-------) | ||
| # S_IRWXG = 0o070 (group rwx) | ||
| # S_IRWXO = 0o007 (other rwx) | ||
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | ||
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" | ||
| assert mode & stat.S_IRUSR, "Owner should have read" | ||
| assert mode & stat.S_IWUSR, "Owner should have write" |
There was a problem hiding this comment.
The current permission check is good, but it could be more robust. It verifies that owner-read and owner-write permissions are set, and group/other permissions are not, but it doesn't ensure that other permission bits (like owner-execute) are unset. A more precise approach is to check that the file mode is exactly 0o600.
| mode = os.stat(plan_file).st_mode | |
| # Check permissions are 600 (rw-------) | |
| # S_IRWXG = 0o070 (group rwx) | |
| # S_IRWXO = 0o007 (other rwx) | |
| assert not (mode & stat.S_IRWXG), "Group should have no permissions" | |
| assert not (mode & stat.S_IRWXO), "Others should have no permissions" | |
| assert mode & stat.S_IRUSR, "Owner should have read" | |
| assert mode & stat.S_IWUSR, "Owner should have write" | |
| permissions = stat.S_IMODE(os.stat(plan_file).st_mode) | |
| assert permissions == 0o600, f"Expected file permissions to be 0o600, but got {oct(permissions)}" |
There was a problem hiding this comment.
Pull request overview
This PR hardens security checks in the CLI by tightening URL validation (rejecting credential-bearing URLs) and attempting to ensure plan.json is written with restrictive permissions, with accompanying tests and a short security note.
Changes:
- Update
validate_folder_urlto reject URLs containing credentials (user:pass@host). - Write
--plan-jsonoutput usingos.open(..., 0o600)+os.fdopen()instead of plainopen(). - Add tests for credential URL rejection and
plan.jsonpermissions; add a Sentinel journal entry documenting the pattern.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.
| File | Description |
|---|---|
main.py |
Adds credential rejection in URL validation; changes plan output file creation to use restrictive permissions. |
test_main.py |
Adds tests for credential-bearing URL rejection and plan.json file permission behavior. |
.jules/sentinel.md |
Documents the secure file creation pattern and rationale. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| log.info("Plan written to %s", args.plan_json) | ||
| try: | ||
| # Securely create file with 600 permissions | ||
| fd = os.open(args.plan_json, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) |
There was a problem hiding this comment.
Creating the file with mode 0o600 only guarantees restrictive permissions when the file is newly created; if args.plan_json already exists with broader permissions, os.open(...|O_TRUNC, 0o600) will not tighten them. Consider applying os.fchmod(fd, 0o600) immediately after opening, or writing to a secure temp file (0600) and os.replace it into place to ensure the final file is always private.
| fd = os.open(args.plan_json, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | |
| fd = os.open(args.plan_json, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600) | |
| try: | |
| # Ensure restrictive permissions even if file already existed | |
| os.fchmod(fd, 0o600) | |
| except OSError: | |
| # If we cannot adjust permissions, proceed as before | |
| pass |
| json.dump(plan, f, indent=2) | ||
| log.info("Plan written to %s", args.plan_json) | ||
| except OSError as e: | ||
| log.error(f"Failed to write plan to {args.plan_json}: {e}") |
There was a problem hiding this comment.
This except OSError logs the failure to write --plan-json but then continues; the process can still exit 0 even though the user explicitly requested an output file. Consider failing the run (e.g., exit(1)/re-raise) when --plan-json is provided but cannot be written.
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| sys.exit(1) |
| # Check that warning message contains "credentials detected" | ||
| # call_args is (args, kwargs) | ||
| args, _ = mock_log.warning.call_args | ||
| assert "credentials detected" in args[0] |
There was a problem hiding this comment.
This test asserts that a warning was logged, but it doesn't verify that credentials were not included in the logged message. Since the new behavior is security-motivated, add an assertion that the warning text does not contain user:pass (or the substring before @) to prevent regressions that leak credentials via logs.
| assert "credentials detected" in args[0] | |
| assert "credentials detected" in args[0] | |
| # Ensure that raw credentials are not leaked in the warning message | |
| assert "user:pass" not in args[0] |
| log.info("Plan written to %s", args.plan_json) | ||
| except OSError as e: | ||
| log.error(f"Failed to write plan to {args.plan_json}: {e}") |
There was a problem hiding this comment.
The error log uses an f-string with the raw args.plan_json path. Since this codebase uses sanitize_for_log to escape control characters and prevent log injection, consider logging sanitize_for_log(args.plan_json) (and sanitize_for_log(e)) and/or using logger formatting (log.error("... %s", ...)) to avoid embedding untrusted strings directly.
| log.info("Plan written to %s", args.plan_json) | |
| except OSError as e: | |
| log.error(f"Failed to write plan to {args.plan_json}: {e}") | |
| log.info("Plan written to %s", sanitize_for_log(args.plan_json)) | |
| except OSError as e: | |
| log.error( | |
| "Failed to write plan to %s: %s", | |
| sanitize_for_log(args.plan_json), | |
| sanitize_for_log(e), | |
| ) |
|
|
||
| if parsed.username or parsed.password: | ||
| log.warning( | ||
| f"Skipping unsafe URL (credentials detected): {sanitize_for_log(url)}" |
There was a problem hiding this comment.
The warning message includes sanitize_for_log(url), but sanitize_for_log only redacts TOKEN and escapes control chars; it does not remove URL credentials. For a URL like https://user:pass@host/..., this will leak user:pass into logs. Avoid logging the raw URL here (log only hostname) or strip credentials from the URL before calling sanitize_for_log (or enhance sanitize_for_log to scrub userinfo@).
| f"Skipping unsafe URL (credentials detected): {sanitize_for_log(url)}" | |
| f"Skipping unsafe URL (credentials detected for host {hostname})" |
…itization, add dry-run plan details Incorporates the best changes from 36 Jules PRs, addressing review feedback: Bolt (Performance) - from PR #173: - Pre-compile PROFILE_ID_PATTERN and RULE_PATTERN at module level - Use compiled patterns in is_valid_profile_id_format, validate_profile_id, and is_valid_rule - Supersedes PRs: #140, #143, #152, #155, #158, #161, #167, #170, #173 Sentinel (Security) - from PR #172 with review feedback: - Enhance sanitize_for_log to redact Basic Auth credentials in URLs - Redact sensitive query parameters (token, key, secret, password, etc.) - Handle fragment separators (#) per Gemini Code Assist review - Use [^&#\s]* pattern per Copilot reviewer suggestion - Update docstring per reviewer suggestion - Supersedes PRs: #142, #145, #148, #151, #154, #157, #160, #169, #172 Palette (UX) - from PR #174 with lint fixes: - Add print_plan_details function for dry-run visibility - Fix duplicate render_progress_bar definition bug - Supersedes PRs: #139, #141, #144, #147, #150, #153, #156, #159, #162, #165, #168, #171, #174 Also: #146, #149, #164 (parallel folder deletion) and #166 (auto-fix .env perms) are independent features not consolidated here. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com>
…itization, add dry-run plan details Incorporates the best changes from 36 Jules PRs, addressing review feedback: Bolt (Performance) - from PR #173: - Pre-compile PROFILE_ID_PATTERN and RULE_PATTERN at module level - Use compiled patterns in is_valid_profile_id_format, validate_profile_id, and is_valid_rule - Supersedes PRs: #140, #143, #152, #155, #158, #161, #167, #170, #173 Sentinel (Security) - from PR #172 with review feedback: - Enhance sanitize_for_log to redact Basic Auth credentials in URLs - Redact sensitive query parameters (token, key, secret, password, etc.) - Handle fragment separators (#) per Gemini Code Assist review - Use [^&#\s]* pattern per Copilot reviewer suggestion - Update docstring per reviewer suggestion - Supersedes PRs: #142, #145, #148, #151, #154, #157, #160, #169, #172 Palette (UX) - from PR #174 with lint fixes: - Add print_plan_details function for dry-run visibility - Fix duplicate render_progress_bar definition bug - Supersedes PRs: #139, #141, #144, #147, #150, #153, #156, #159, #162, #165, #168, #171, #174 Also: #146, #149, #164 (parallel folder deletion) and #166 (auto-fix .env perms) are independent features not consolidated here. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com>
Refined
validate_folder_urlto reject URLs with credentials (user:pass@host) to prevent accidental leakage. Securedplan.jsoncreation usingos.openwith0o600permissions to prevent unauthorized access. Added comprehensive tests intest_main.py.PR created automatically by Jules for task 17198079295871683585 started by @abhimehro