From 7a839f73e6e2214929982e1c810e0815aea1a19d Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Fri, 6 Feb 2026 11:03:44 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[MEDIUM]=20?= =?UTF-8?q?Auto-fix=20insecure=20.env=20permissions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Automatically remediate insecure .env file permissions (world-readable) by setting them to 600 (owner read/write only). Falls back to warning if fix fails. Why: - Secrets (tokens) are stored in .env. - Loose permissions allow other users on the system to steal secrets. - Auto-fixing is safer and friendlier than just warning. Co-authored-by: abhimehro <84992105+abhimehro@users.noreply.github.com> --- main.py | 11 ++++ tests/test_permissions.py | 128 ++++++++++++++++++++++++++++++++++++++ tests/test_security.py | 16 ++--- 3 files changed, 148 insertions(+), 7 deletions(-) create mode 100644 tests/test_permissions.py diff --git a/main.py b/main.py index 86792da..4d3cece 100644 --- a/main.py +++ b/main.py @@ -120,7 +120,18 @@ def check_env_permissions(env_path: str = ".env") -> None: "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" + ) + return + except Exception as e: + 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 " diff --git a/tests/test_permissions.py b/tests/test_permissions.py new file mode 100644 index 0000000..f96bff0 --- /dev/null +++ b/tests/test_permissions.py @@ -0,0 +1,128 @@ +import os +import stat +import sys +from unittest.mock import MagicMock +import main + +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" + +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): + 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" + +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 + # 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" diff --git a/tests/test_security.py b/tests/test_security.py index 536852c..bda526e 100644 --- a/tests/test_security.py +++ b/tests/test_security.py @@ -100,8 +100,8 @@ def test_push_rules_filters_xss_payloads(): @pytest.mark.skipif( os.name == "nt", reason="Unix permissions not applicable on Windows" ) -def test_env_permission_check_warns_on_insecure_permissions(monkeypatch, tmp_path): - """Test that insecure .env permissions trigger a warning.""" +def test_env_permission_check_fixes_insecure_permissions(monkeypatch, tmp_path): + """Test that insecure .env permissions are automatically fixed.""" # Import main to get access to check_env_permissions and Colors import main @@ -110,19 +110,21 @@ def test_env_permission_check_warns_on_insecure_permissions(monkeypatch, tmp_pat env_file.write_text("TOKEN=test") os.chmod(env_file, 0o644) - # Mock sys.stderr to capture warnings + # Mock sys.stderr to capture output mock_stderr = MagicMock() monkeypatch.setattr(sys, "stderr", mock_stderr) # Run the permission check logic main.check_env_permissions(str(env_file)) - # Verify warning was written + # Verify it fixed the file + assert stat.S_IMODE(os.stat(env_file).st_mode) == 0o600 + + # Verify success message was written mock_stderr.write.assert_called() call_args = mock_stderr.write.call_args[0][0] - assert "Security Warning" in call_args - assert "readable by others" in call_args - assert "644" in call_args + assert "Fixed .env permissions" in call_args + assert "set to 600" in call_args @pytest.mark.skipif(