Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (104/100) Warning

Line too long (104/100)

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (104/100) Warning

Line too long (104/100)
)
return
except Exception as e:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

It's better to catch a more specific exception than the generic Exception. os.chmod raises OSError on errors (including PermissionError, which is a subclass). Catching OSError will prevent masking other unrelated errors (like KeyboardInterrupt or other programming errors) and make the error handling more robust.

Suggested change
except Exception as e:
except OSError 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 "
Expand Down Expand Up @@ -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 warning

Code 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()
Expand Down Expand Up @@ -1182,7 +1193,7 @@
folder_data,
profile_id,
existing_rules,
client, # Pass the persistent client

Check notice

Code 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
}
Expand All @@ -1192,7 +1203,7 @@
folder_name = folder_data["group"]["group"].strip()
try:
if future.result():
success_count += 1

Check warning

Code 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 notice

Code 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)}"
Expand Down
128 changes: 128 additions & 0 deletions tests/test_permissions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import os

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring Warning test

Missing module docstring

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring Warning test

Missing module docstring
import stat
import sys
from unittest.mock import MagicMock
import main

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To reduce code duplication across your test functions, consider using a pytest fixture to handle the common mocking setup (os.name, os.path.exists, os.stat, os.chmod, sys.stderr). This will make your tests cleaner and easier to maintain.

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 notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +37 to +42

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This loop to find the message in stderr can be simplified. Since a successful fix should only write to stderr once before returning, you can assert that write was called once and then check the content of that single call. This makes the assertion more direct and readable.

    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 warning

Code scanning / Pylint (reported by Codacy)

Missing function docstring Warning test

Missing function docstring

Check notice

Code 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 notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +65 to +71

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the previous test, this loop can be simplified. When the auto-fix fails, the function writes a single warning message to stderr. You can assert that write was called once and then inspect its content directly.

    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 warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (115/100) Warning test

Line too long (115/100)

Check warning

Code 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 notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +122 to +128

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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

16 changes: 9 additions & 7 deletions tests/test_security.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@
@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

Expand All @@ -110,19 +110,21 @@
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

Check notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.

# 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

Check notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
assert "set to 600" in call_args

Check notice

Code 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 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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.


@pytest.mark.skipif(
Expand Down
Loading