-
Notifications
You must be signed in to change notification settings - Fork 1
🛡️ Sentinel: [MEDIUM] Auto-fix insecure .env permissions #166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -120,7 +120,18 @@ | |||||
| "Please secure your .env file so it is only readable by " "the owner." | ||||||
| ) | ||||||
| if os.name != "nt": | ||||||
| try: | ||||||
| # Auto-fix permissions on Unix-like systems | ||||||
| os.chmod(env_path, stat.S_IRUSR | stat.S_IWUSR) | ||||||
| sys.stderr.write( | ||||||
| f"{Colors.GREEN}🔒 Security: Fixed .env permissions (set to 600).{Colors.ENDC}\n" | ||||||
Check warningCode scanning / Pylint (reported by Codacy) Line too long (104/100) Warning
Line too long (104/100)
|
||||||
| ) | ||||||
| return | ||||||
| except Exception as e: | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's better to catch a more specific exception than the generic
Suggested change
|
||||||
| platform_hint += f" (Auto-fix failed: {e})" | ||||||
| else: | ||||||
| platform_hint += " For example: 'chmod 600 .env'." | ||||||
|
|
||||||
| perms = format(stat.S_IMODE(file_stat.st_mode), "03o") | ||||||
| sys.stderr.write( | ||||||
| f"{Colors.WARNING}⚠️ Security Warning: .env file is " | ||||||
|
|
@@ -677,7 +688,7 @@ | |||||
|
|
||||||
| # Parallelize fetching rules from folders. | ||||||
| # Using 5 workers to be safe with rate limits, though GETs are usually cheaper. | ||||||
| with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: | ||||||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Variable name "e" doesn't conform to snake_case naming style Warning
Variable name "e" doesn't conform to snake_case naming style
|
||||||
| future_to_folder = { | ||||||
| executor.submit(_fetch_folder_rules, folder_id): folder_id | ||||||
| for folder_name, folder_id in folders.items() | ||||||
|
|
@@ -1182,7 +1193,7 @@ | |||||
| folder_data, | ||||||
| profile_id, | ||||||
| existing_rules, | ||||||
| client, # Pass the persistent client | ||||||
Check noticeCode scanning / Pylint (reported by Codacy) Catching too general exception Exception Note
Catching too general exception Exception
|
||||||
| ): folder_data | ||||||
| for folder_data in folder_data_list | ||||||
| } | ||||||
|
|
@@ -1192,7 +1203,7 @@ | |||||
| folder_name = folder_data["group"]["group"].strip() | ||||||
| try: | ||||||
| if future.result(): | ||||||
| success_count += 1 | ||||||
Check warningCode scanning / Pylint (reported by Codacy) Variable name "e" doesn't conform to snake_case naming style Warning
Variable name "e" doesn't conform to snake_case naming style
Check noticeCode scanning / Pylintpython3 (reported by Codacy) Catching too general exception Exception Note
Catching too general exception Exception
|
||||||
| except Exception as e: | ||||||
| log.error( | ||||||
| f"Failed to process folder '{sanitize_for_log(folder_name)}': {sanitize_for_log(e)}" | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,128 @@ | ||
| import os | ||
Check warningCode scanning / Pylint (reported by Codacy) Missing module docstring Warning test
Missing module docstring
Check warningCode scanning / Pylintpython3 (reported by Codacy) Missing module docstring Warning test
Missing module docstring
|
||
| import stat | ||
| import sys | ||
| from unittest.mock import MagicMock | ||
| import main | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. To reduce code duplication across your test functions, consider using a Here's an example of how you could define such a fixture: import pytest
@pytest.fixture
def mock_env_check(monkeypatch):
"""Fixture for mocking environment for permission checks."""
monkeypatch.setattr(os, "name", "posix")
monkeypatch.setattr(os.path, "exists", lambda _: True)
mock_stat_obj = MagicMock()
monkeypatch.setattr(os, "stat", lambda _: mock_stat_obj)
mock_chmod = MagicMock()
monkeypatch.setattr(os, "chmod", mock_chmod)
mock_stderr = MagicMock()
monkeypatch.setattr(sys, "stderr", mock_stderr)
class Mocks:
stat = mock_stat_obj
chmod = mock_chmod
stderr = mock_stderr
return Mocks()You could then refactor your tests to use it, which would simplify the setup in each test. |
||
| def test_check_env_permissions_fixes_loose_permissions(monkeypatch): | ||
| """Test that check_env_permissions attempts to fix loose permissions.""" | ||
|
|
||
| # Mock os.name to be 'posix' (non-nt) | ||
| monkeypatch.setattr(os, "name", "posix") | ||
|
|
||
| # Mock os.path.exists to return True | ||
| monkeypatch.setattr(os.path, "exists", lambda x: True) | ||
|
|
||
| # Mock os.stat to return loose permissions (e.g., 777) | ||
| mock_stat = MagicMock() | ||
| mock_stat.st_mode = 0o777 | ||
| monkeypatch.setattr(os, "stat", lambda x: mock_stat) | ||
|
|
||
| # Mock os.chmod | ||
| mock_chmod = MagicMock() | ||
| monkeypatch.setattr(os, "chmod", mock_chmod) | ||
|
|
||
| # Mock sys.stderr to capture output | ||
| mock_stderr = MagicMock() | ||
| monkeypatch.setattr(sys, "stderr", mock_stderr) | ||
|
|
||
| # Run | ||
| main.check_env_permissions(".env") | ||
|
|
||
| # Assert chmod was called with 600 (stat.S_IRUSR | stat.S_IWUSR) | ||
| mock_chmod.assert_called_once_with(".env", stat.S_IRUSR | stat.S_IWUSR) | ||
|
|
||
| # Assert success message logged | ||
| # We check if at least one call contained the success message | ||
| found = False | ||
| for call_args in mock_stderr.write.call_args_list: | ||
| if "Fixed .env permissions" in call_args[0][0] and "set to 600" in call_args[0][0]: | ||
| found = True | ||
| break | ||
| assert found, "Success message not found in stderr writes" | ||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines
+37
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loop to find the message in mock_stderr.write.assert_called_once()
output = mock_stderr.write.call_args[0][0]
assert "Fixed .env permissions" in output and "set to 600" in output |
||
|
|
||
| def test_check_env_permissions_warns_on_fix_failure(monkeypatch): | ||
| """Test that it warns if chmod fails.""" | ||
|
|
||
| monkeypatch.setattr(os, "name", "posix") | ||
| monkeypatch.setattr(os.path, "exists", lambda x: True) | ||
|
|
||
| mock_stat = MagicMock() | ||
| mock_stat.st_mode = 0o777 | ||
| monkeypatch.setattr(os, "stat", lambda x: mock_stat) | ||
|
|
||
| # Mock chmod to raise exception | ||
| def raise_error(*args): | ||
Check warningCode scanning / Pylint (reported by Codacy) Missing function docstring Warning test
Missing function docstring
Check noticeCode scanning / Pylint (reported by Codacy) Unused argument 'args' Note test
Unused argument 'args'
|
||
| raise PermissionError("Access denied") | ||
| monkeypatch.setattr(os, "chmod", raise_error) | ||
|
|
||
| mock_stderr = MagicMock() | ||
| monkeypatch.setattr(sys, "stderr", mock_stderr) | ||
|
|
||
| main.check_env_permissions(".env") | ||
|
|
||
| # Assert warning message logged with failure hint | ||
| found = False | ||
| for call_args in mock_stderr.write.call_args_list: | ||
| msg = call_args[0][0] | ||
| if "Security Warning" in msg and "Auto-fix failed" in msg: | ||
| found = True | ||
| break | ||
| assert found, "Failure warning not found in stderr writes" | ||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines
+65
to
+71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to the previous test, this loop can be simplified. When the auto-fix fails, the function writes a single warning message to mock_stderr.write.assert_called_once()
output = mock_stderr.write.call_args[0][0]
assert "Security Warning" in output and "Auto-fix failed" in output |
||
|
|
||
| def test_check_env_permissions_ignores_secure_permissions(monkeypatch): | ||
| """Test that it does nothing if permissions are already secure.""" | ||
|
|
||
| monkeypatch.setattr(os, "name", "posix") | ||
| monkeypatch.setattr(os.path, "exists", lambda x: True) | ||
|
|
||
| # 0o600 is S_IRUSR | S_IWUSR | ||
| # os.stat returns st_mode which includes file type bits, but check_env_permissions masks with S_IRWXG | S_IRWXO | ||
Check warningCode scanning / Pylintpython3 (reported by Codacy) Line too long (115/100) Warning test
Line too long (115/100)
Check warningCode scanning / Pylint (reported by Codacy) Line too long (115/100) Warning test
Line too long (115/100)
|
||
| # So we just need to ensure the group/other bits are 0. | ||
| mock_stat = MagicMock() | ||
| mock_stat.st_mode = stat.S_IRUSR | stat.S_IWUSR # 600 | ||
| monkeypatch.setattr(os, "stat", lambda x: mock_stat) | ||
|
|
||
| mock_chmod = MagicMock() | ||
| monkeypatch.setattr(os, "chmod", mock_chmod) | ||
|
|
||
| mock_stderr = MagicMock() | ||
| monkeypatch.setattr(sys, "stderr", mock_stderr) | ||
|
|
||
| main.check_env_permissions(".env") | ||
|
|
||
| # Assert chmod NOT called | ||
| mock_chmod.assert_not_called() | ||
|
|
||
| # Assert nothing written to stderr | ||
| mock_stderr.write.assert_not_called() | ||
|
|
||
| def test_check_env_permissions_warns_on_windows(monkeypatch): | ||
| """Test that it only warns (no fix attempt) on Windows.""" | ||
|
|
||
| monkeypatch.setattr(os, "name", "nt") | ||
| monkeypatch.setattr(os.path, "exists", lambda x: True) | ||
|
|
||
| mock_stat = MagicMock() | ||
| mock_stat.st_mode = 0o777 | ||
| monkeypatch.setattr(os, "stat", lambda x: mock_stat) | ||
|
|
||
| mock_chmod = MagicMock() | ||
| monkeypatch.setattr(os, "chmod", mock_chmod) | ||
|
|
||
| mock_stderr = MagicMock() | ||
| monkeypatch.setattr(sys, "stderr", mock_stderr) | ||
|
|
||
| main.check_env_permissions(".env") | ||
|
|
||
| # Assert chmod NOT called | ||
| mock_chmod.assert_not_called() | ||
|
|
||
| # Assert warning message logged | ||
| found = False | ||
| for call_args in mock_stderr.write.call_args_list: | ||
| msg = call_args[0][0] | ||
| if "Security Warning" in msg and "chmod 600 .env" in msg: | ||
| found = True | ||
| break | ||
| assert found, "Windows warning not found" | ||
Check noticeCode scanning / Bandit Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Check noticeCode scanning / Bandit (reported by Codacy) Use of assert detected. The enclosed code will be removed when compiling to optimised byte code. Note test
Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines
+122
to
+128
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This loop can also be simplified. On Windows, the function should write a single warning message. A direct assertion on a single call is cleaner and more precise. mock_stderr.write.assert_called_once()
output = mock_stderr.write.call_args[0][0]
assert "Security Warning" in output and "chmod 600 .env" in output |
||
Check warning
Code scanning / Pylintpython3 (reported by Codacy)
Line too long (104/100) Warning