Skip to content
Merged
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
8 changes: 8 additions & 0 deletions .jules/sentinel.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,11 @@
**Learning:** `httpx.HTTPStatusError` (raised by `raise_for_status()`) inherits from `httpx.HTTPError`. Generic `except httpx.HTTPError:` blocks will catch it and retry client errors unless explicitly handled.

**Prevention:** Inside retry loops, catch `httpx.HTTPStatusError` first. Check `response.status_code`. If `400 <= code < 500` (and not `429`), re-raise immediately.

## 2026-02-09 - Insecure Symlink Follow in Permission Fix

**Vulnerability:** The `check_env_permissions` function used `os.chmod` on `.env` without checking if it was a symlink. An attacker could symlink `.env` to a system file (e.g., `/etc/passwd`), causing the script to change its permissions to 600, leading to Denial of Service or security issues. Additionally, `fix_env.py` overwrote `.env` insecurely, allowing arbitrary file overwrite via symlink.

**Learning:** `os.chmod` and `open()` follow symlinks by default in Python (and most POSIX environments).

**Prevention:** Always use `os.path.islink()` to check for symlinks before modifying file metadata or content if the path is user-controlled or in a writable directory. Use `os.open` with `O_CREAT | O_WRONLY | O_TRUNC` and `os.chmod(fd)` on the file descriptor to avoid race conditions (TOCTOU) and ensure operations apply to the file itself, not a symlink target.
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The β€œPrevention” guidance suggests that os.open(..., O_CREAT|O_WRONLY|O_TRUNC) + os.chmod(fd) avoids TOCTOU/symlink attacks, but that isn’t sufficient unless symlink-following is explicitly prevented (e.g., O_NOFOLLOW where supported and/or fstat-based validation of a regular file). Please update this write-up to reflect the need for O_NOFOLLOW/fstat (and ideally os.fchmod) so the documented guidance matches the actual secure pattern.

Suggested change
**Prevention:** Always use `os.path.islink()` to check for symlinks before modifying file metadata or content if the path is user-controlled or in a writable directory. Use `os.open` with `O_CREAT | O_WRONLY | O_TRUNC` and `os.chmod(fd)` on the file descriptor to avoid race conditions (TOCTOU) and ensure operations apply to the file itself, not a symlink target.
**Prevention:** Treat any path in a user-writable location as attacker-controlled. `os.path.islink()` can be used as an initial guard, but it is not sufficient on its own because of TOCTOU races. For creating or overwriting files like `.env`, open them with `os.open` using `O_CREAT | O_WRONLY | O_TRUNC` combined with `O_NOFOLLOW` where supported, then use `os.fstat(fd)` to verify you actually opened a regular file (and not a symlink or special file), and finally call `os.fchmod(fd, 0o600)` (or your desired mode) on the file descriptor. This descriptor-based pattern avoids following symlinks and prevents race conditions between path resolution and permission changes.

Copilot uses AI. Check for mistakes.
28 changes: 22 additions & 6 deletions fix_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,28 @@ def escape_val(val):
# Write back with standard quotes
new_content = f'TOKEN="{escape_val(real_token)}"\nPROFILE="{escape_val(real_profiles)}"\n'

with open('.env', 'w') as f:
f.write(new_content)

# SECURITY: Ensure .env is only readable by the owner (600) on Unix-like systems
if os.name != 'nt':
os.chmod('.env', stat.S_IRUSR | stat.S_IWUSR)
# Security: Check for symlinks to prevent overwriting arbitrary files
if os.path.islink('.env'):
print("Security Warning: .env is a symlink. Aborting to avoid overwriting target.")
return
Comment on lines +62 to +65
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

The symlink check happens only right before writing, but the function reads .env at the top via open('.env', 'r'), which still follows symlinks. If .env is a symlink, this will read the target file (unexpected and potentially sensitive) before aborting. Move the symlink check (and ideally a regular-file check) before any read of .env.

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +65

Choose a reason for hiding this comment

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

critical

This os.path.islink check is a critical security measure to prevent attackers from manipulating .env to point to sensitive system files. Excellent addition.


# Security: Write using os.open to ensure 600 permissions at creation time
# This prevents a race condition where the file is world-readable before chmod
try:
# O_TRUNC to overwrite if exists, O_CREAT to create if not
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600

fd = os.open('.env', flags, mode)

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "fd" doesn't conform to snake_case naming style

Variable name "fd" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "fd" doesn't conform to snake_case naming style

Variable name "fd" doesn't conform to snake_case naming style
with os.fdopen(fd, 'w') as f:

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "f" doesn't conform to snake_case naming style

Variable name "f" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "f" doesn't conform to snake_case naming style

Variable name "f" doesn't conform to snake_case naming style
f.write(new_content)
# Enforce permissions on the file descriptor directly (safe against race conditions)
if os.name != 'nt':
os.chmod(fd, mode)
Comment on lines +71 to +79
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

os.path.islink() alone doesn’t prevent a race: an attacker can swap .env to a symlink between the check and os.open(..., O_TRUNC), still allowing overwrite of an arbitrary target. To actually prevent symlink-following, include O_NOFOLLOW when available (via getattr(os, 'O_NOFOLLOW', 0)) and/or validate os.fstat(fd) is a regular file before writing; also prefer os.fchmod(fd, mode) over os.chmod(fd, mode) for clarity/portability on POSIX.

Suggested change
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
fd = os.open('.env', flags, mode)
with os.fdopen(fd, 'w') as f:
f.write(new_content)
# Enforce permissions on the file descriptor directly (safe against race conditions)
if os.name != 'nt':
os.chmod(fd, mode)
# Include O_NOFOLLOW when available to prevent following symlinks at open-time
flags = os.O_WRONLY | os.O_CREAT | os.O_TRUNC | getattr(os, 'O_NOFOLLOW', 0)
mode = stat.S_IRUSR | stat.S_IWUSR # 0o600
fd = os.open('.env', flags, mode)
# Additional safety: verify the opened target is a regular file, not a symlink/device/FIFO
st = os.fstat(fd)
if not stat.S_ISREG(st.st_mode):
# Avoid writing to anything that isn't a normal file
os.close(fd)
print("Security Warning: .env is not a regular file. Aborting write.")
return
with os.fdopen(fd, 'w') as f:
f.write(new_content)
# Enforce permissions on the file descriptor directly (safe against race conditions)
if os.name != 'nt' and hasattr(os, 'fchmod'):
os.fchmod(fd, mode)

Copilot uses AI. Check for mistakes.

except OSError as e:

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style

Variable name "e" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "e" doesn't conform to snake_case naming style

Variable name "e" doesn't conform to snake_case naming style
print(f"Error writing .env: {e}")
return
Comment on lines +67 to +83

Choose a reason for hiding this comment

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

critical

The use of os.open with O_CREAT | O_WRONLY | O_TRUNC and subsequent os.chmod(fd, mode) is a robust way to prevent TOCTOU race conditions. This ensures that the file is created with the correct permissions atomically and that operations are performed on the intended file, not a symlink target. This is a significant security improvement.


print("Fixed .env file: standardized quotes and corrected variable assignments.")
print("Security: .env permissions set to 600 (read/write only by owner).")
Expand Down
9 changes: 9 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ def check_env_permissions(env_path: str = ".env") -> None:
if not os.path.exists(env_path):
return

# Security: Don't follow symlinks when checking/fixing permissions
# This prevents attacks where .env is symlinked to a system file (e.g., /etc/passwd)
if os.path.islink(env_path):
sys.stderr.write(
f"{Colors.WARNING}⚠️ Security Warning: {env_path} is a symlink. "
f"Skipping permission fix to avoid damaging target file.{Colors.ENDC}\n"
)
return
Comment on lines 116 to +126
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

os.path.exists() follows symlinks, so a dangling .env symlink will return False and skip the new symlink warning entirely. Also, checking islink() and then later calling os.stat/os.chmod on the path is still vulnerable to a TOCTOU swap (symlink can be replaced after the check). Consider using os.path.lexists() and performing the symlink check before the existence check, and/or switching to an FD-based approach (e.g., os.open with O_NOFOLLOW where available + os.fstat/os.fchmod, or os.chmod(..., follow_symlinks=False) as a fallback) to ensure the target cannot be replaced between check and chmod.

Copilot uses AI. Check for mistakes.
Comment on lines +119 to +126

Choose a reason for hiding this comment

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

critical

Adding the os.path.islink check here is essential to prevent check_env_permissions from inadvertently modifying permissions of a symlinked system file. This directly addresses the described vulnerability.


# Windows doesn't have Unix permissions
if os.name == "nt":
# Just warn on Windows, can't auto-fix
Expand Down
80 changes: 80 additions & 0 deletions tests/test_fix_env.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import os

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring

Missing module docstring

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring

Missing module docstring
import stat

Check warning

Code scanning / Prospector (reported by Codacy)

Unused import stat (unused-import)

Unused import stat (unused-import)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused import stat

Unused import stat

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused import stat

Unused import stat
import pytest

Check warning

Code scanning / Prospector (reported by Codacy)

Unable to import 'pytest' (import-error)

Unable to import 'pytest' (import-error)
from unittest.mock import MagicMock, patch

Check warning

Code scanning / Prospector (reported by Codacy)

Unused MagicMock imported from unittest.mock (unused-import)

Unused MagicMock imported from unittest.mock (unused-import)

Check warning

Code scanning / Pylint (reported by Codacy)

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

Check notice

Code scanning / Pylint (reported by Codacy)

Unused MagicMock imported from unittest.mock

Unused MagicMock imported from unittest.mock

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused MagicMock imported from unittest.mock

Unused MagicMock imported from unittest.mock
Comment on lines +2 to +4
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

There are unused imports here (stat, MagicMock) which can fail linting in CI and add noise. Please remove any imports that aren’t used.

Suggested change
import stat
import pytest
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import patch

Copilot uses AI. Check for mistakes.
import fix_env

def test_fix_env_skips_symlink(tmp_path):
"""
Verify that fix_env skips symlinks and logs a warning.
This prevents overwriting the target file.
"""
# Create a target file
target_file = tmp_path / "target_file"
target_file.write_text("TOKEN=foo\nPROFILE=bar")

cwd = os.getcwd()
os.chdir(tmp_path)
try:
symlink = tmp_path / ".env"
try:
os.symlink(target_file.name, symlink.name)
except OSError:
pytest.skip("Symlinks not supported")

# Mock print to verify warning
with patch("builtins.print") as mock_print:
fix_env.fix_env()

# Verify warning was printed
assert mock_print.called

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.
found = False
for call_args in mock_print.call_args_list:
msg = call_args[0][0]
if "Security Warning" in msg and "symlink" in msg:
found = True
break
assert found, "Warning about symlink not found"

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

Choose a reason for hiding this comment

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

medium

The assertion for the warning message can be made more concise and readable using mock_print.assert_any_call. This directly checks if the expected warning message was printed.

Suggested change
assert mock_print.called
found = False
for call_args in mock_print.call_args_list:
msg = call_args[0][0]
if "Security Warning" in msg and "symlink" in msg:
found = True
break
assert found, "Warning about symlink not found"
mock_print.assert_any_call("Security Warning: .env is a symlink. Aborting to avoid overwriting target.")

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.

# Verify target file content is UNCHANGED
assert target_file.read_text() == "TOKEN=foo\nPROFILE=bar"

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.

finally:
os.chdir(cwd)

def test_fix_env_creates_secure_file(tmp_path):
"""
Verify that fix_env creates .env with 600 permissions.
"""
if os.name == 'nt':
pytest.skip("Permission check not supported on Windows")

cwd = os.getcwd()
os.chdir(tmp_path)
try:
# Use realistic token (starts with api. or long)
# nosec - testing token, not real secret
token = "api.1234567890abcdef"

Check notice

Code scanning / Bandit

Possible hardcoded password: 'api.1234567890abcdef' Note test

Possible hardcoded password: 'api.1234567890abcdef'
profile = "12345abc"

with open(".env", "w") as f:

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "f" doesn't conform to snake_case naming style

Variable name "f" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "f" doesn't conform to snake_case naming style

Variable name "f" doesn't conform to snake_case naming style
f.write(f"TOKEN={token}\nPROFILE={profile}")

# Set permissions to 644 (world-readable), which should be fixed to 600
# 0o777 is flagged by bandit B103
os.chmod(".env", 0o644)

# Run fix_env
fix_env.fix_env()

# Verify permissions are 600
st = os.stat(".env")

Check warning

Code scanning / Pylint (reported by Codacy)

Variable name "st" doesn't conform to snake_case naming style

Variable name "st" doesn't conform to snake_case naming style

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Variable name "st" doesn't conform to snake_case naming style

Variable name "st" doesn't conform to snake_case naming style
assert (st.st_mode & 0o777) == 0o600

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.

# Verify content is fixed and quoted
content = open(".env").read()
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

open('.env').read() leaves the file handle unclosed. Use a context manager (with open(...) as f) to avoid resource warnings and keep the pattern consistent with the rest of the test.

Suggested change
content = open(".env").read()
with open(".env") as f:
content = f.read()

Copilot uses AI. Check for mistakes.
assert f'TOKEN="{token}"' in content

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.
assert f'PROFILE="{profile}"' in content

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.

finally:
os.chdir(cwd)
40 changes: 29 additions & 11 deletions tests/test_plan_details.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
"""Tests for the print_plan_details dry-run output function."""

import importlib
import os
import sys
from unittest.mock import patch

import main
Expand Down Expand Up @@ -42,16 +45,31 @@

def test_print_plan_details_with_colors(capsys):
"""Test print_plan_details output when colors are enabled."""
with patch("main.USE_COLORS", True):
plan_entry = {
"profile": "test_profile",
"folders": [{"name": "Folder A", "rules": 10}],
}
main.print_plan_details(plan_entry)
# Force USE_COLORS=True for this test, but also ensure Colors class is populated
# The Colors class is defined at import time based on USE_COLORS.
# If main was imported previously with USE_COLORS=False, Colors attributes are empty strings.
# We must reload main with an environment that forces USE_COLORS=True, or mock Colors.

captured = capsys.readouterr()
output = captured.out
with patch.dict(os.environ, {"NO_COLOR": ""}):
with patch("sys.stderr.isatty", return_value=True), patch("sys.stdout.isatty", return_value=True):

Check warning

Code scanning / Pylint (reported by Codacy)

Line too long (106/100)

Line too long (106/100)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Line too long (106/100)

Line too long (106/100)
# Robust reload: handle case where main module reference is stale

Check notice

Code scanning / Bandit (reported by Codacy)

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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
Comment on lines +53 to +55
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

patch("sys.stderr.isatty", ...) / patch("sys.stdout.isatty", ...) is likely to raise AttributeError because sys.stderr/sys.stdout are typically io.TextIOWrapper instances that don’t allow setting arbitrary attributes (no instance __dict__). Patch sys.stderr/sys.stdout themselves with objects that implement isatty() (or patch main.USE_COLORS/main.Colors directly) to make this test reliable.

Copilot uses AI. Check for mistakes.
if "main" in sys.modules:

Check notice

Code scanning / Bandit (reported by Codacy)

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

Use of assert detected. The enclosed code will be removed when compiling to optimised byte code.
importlib.reload(sys.modules["main"])

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

Check warning

Code scanning / Prospector (reported by Codacy)

Reimport 'main' (imported line 8) (reimported)

Reimport 'main' (imported line 8) (reimported)

Check warning

Code scanning / Prospector (reported by Codacy)

Import outside toplevel (main) (import-outside-toplevel)

Import outside toplevel (main) (import-outside-toplevel)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Import outside toplevel (main)

Import outside toplevel (main)

Check notice

Code scanning / Pylint (reported by Codacy)

Redefining name 'main' from outer scope (line 8)

Redefining name 'main' from outer scope (line 8)

Check notice

Code scanning / Pylint (reported by Codacy)

Reimport 'main' (imported line 8)

Reimport 'main' (imported line 8)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Reimport 'main' (imported line 8)

Reimport 'main' (imported line 8)

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Redefining name 'main' from outer scope (line 8)

Redefining name 'main' from outer scope (line 8)
importlib.reload(main)
Comment on lines +55 to +60
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This import of module main is redundant, as it was previously imported on line 8.

Suggested change
# Robust reload: handle case where main module reference is stale
if "main" in sys.modules:
importlib.reload(sys.modules["main"])
else:
import main
importlib.reload(main)
# Reload main so that the Colors configuration is recomputed
importlib.reload(main)

Copilot uses AI. Check for mistakes.
Comment on lines +56 to +60

Choose a reason for hiding this comment

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

medium

Since import main is already present at the top of the file, sys.modules["main"] will always exist. The if/else block can be simplified to just importlib.reload(sys.modules["main"]) for better readability.

            importlib.reload(sys.modules["main"])


# Now verify output with colors
plan_entry = {
"profile": "test_profile",
"folders": [{"name": "Folder A", "rules": 10}],
}
# Use the module from sys.modules to ensure we use the reloaded one
sys.modules["main"].print_plan_details(plan_entry)

captured = capsys.readouterr()
output = captured.out

assert "πŸ“ Plan Details for test_profile:" in output
assert "Folder A" in output
assert "10 rules" in output
assert "πŸ“ Plan Details for test_profile:" in output

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.
assert "Folder A" in output

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.
assert "10 rules" in output

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.
67 changes: 67 additions & 0 deletions tests/test_symlink.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import os

Check warning

Code scanning / Pylint (reported by Codacy)

Missing module docstring

Missing module docstring

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

Missing module docstring

Missing module docstring
import stat

Check warning

Code scanning / Prospector (reported by Codacy)

Unused import stat (unused-import)

Unused import stat (unused-import)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused import stat

Unused import stat

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused import stat

Unused import stat
import pytest

Check warning

Code scanning / Prospector (reported by Codacy)

Unable to import 'pytest' (import-error)

Unable to import 'pytest' (import-error)
from unittest.mock import MagicMock, patch

Check warning

Code scanning / Pylint (reported by Codacy)

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

Check warning

Code scanning / Prospector (reported by Codacy)

Unused MagicMock imported from unittest.mock (unused-import)

Unused MagicMock imported from unittest.mock (unused-import)

Check warning

Code scanning / Pylintpython3 (reported by Codacy)

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

standard import "from unittest.mock import MagicMock, patch" should be placed before "import pytest"

Check notice

Code scanning / Pylint (reported by Codacy)

Unused MagicMock imported from unittest.mock

Unused MagicMock imported from unittest.mock

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused MagicMock imported from unittest.mock

Unused MagicMock imported from unittest.mock
Comment on lines +2 to +4
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

There are unused imports here (stat, MagicMock) which can fail linting in CI and make the test harder to read. Please remove any imports that aren’t used.

Suggested change
import stat
import pytest
from unittest.mock import MagicMock, patch
import pytest
from unittest.mock import patch

Copilot uses AI. Check for mistakes.
import main

Comment on lines +5 to +6
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

main is imported at module import time. Other tests in this repo import main inside the test function to avoid import-time side effects (e.g., load_dotenv() / TTY-derived globals) during test collection. Please move the import main into the individual tests (or import check_env_permissions inside the tests) to keep collection side-effect free and reduce order-dependence.

Copilot uses AI. Check for mistakes.
def test_check_env_permissions_skips_symlink(tmp_path):
"""
Verify that check_env_permissions skips symlinks and logs a warning.
This prevents modifying permissions of the symlink target.
"""
# Create a target file
target_file = tmp_path / "target_file"
target_file.write_text("target content")

# Set permissions to 644 (world-readable)
target_file.chmod(0o644)
initial_mode = target_file.stat().st_mode
Comment on lines +7 to +18
Copy link

Copilot AI Feb 10, 2026

Choose a reason for hiding this comment

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

This test mutates real filesystem permissions (chmod(0o777) / mode assertions) but doesn’t skip on Windows. On Windows, chmod semantics differ and symlink creation may require privileges, making this test flaky. Consider skipping on os.name == 'nt' (similar to the second test) or rewriting it to mock os.stat/os.chmod like tests/test_env_permissions.py does.

Copilot uses AI. Check for mistakes.

# Create a symlink to the target
symlink = tmp_path / ".env_symlink"
try:
os.symlink(target_file, symlink)
except OSError:
pytest.skip("Symlinks not supported on this platform")

# Mock stderr to verify warning
with patch("sys.stderr") as mock_stderr:
# Run check_env_permissions on the symlink
main.check_env_permissions(str(symlink))

# Verify warning was logged
assert mock_stderr.write.called

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.
warning_msg = mock_stderr.write.call_args[0][0]
assert "Security Warning" in warning_msg

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.
assert "is a symlink" in warning_msg

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.

# Verify target permissions are UNCHANGED
final_mode = target_file.stat().st_mode
assert final_mode == initial_mode

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

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.
assert (final_mode & 0o777) == 0o644 # Still 644, not 600

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

Choose a reason for hiding this comment

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

high

This test effectively verifies that check_env_permissions correctly handles symlinks by skipping permission modification and logging a warning. It's a well-structured test for the security fix.

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.

def test_check_env_permissions_fixes_file(tmp_path):
"""
Verify that check_env_permissions fixes permissions for a regular file.
"""
if os.name == 'nt':
pytest.skip("Permission fix not supported on Windows")

# Create a regular file
env_file = tmp_path / ".env_file"
env_file.write_text("content")

# Set permissions to 644 (world-readable)
env_file.chmod(0o644)

# Run check_env_permissions
with patch("sys.stderr") as mock_stderr:

Check warning

Code scanning / Prospector (reported by Codacy)

Unused variable 'mock_stderr' (unused-variable)

Unused variable 'mock_stderr' (unused-variable)

Check notice

Code scanning / Pylint (reported by Codacy)

Unused variable 'mock_stderr'

Unused variable 'mock_stderr'

Check notice

Code scanning / Pylintpython3 (reported by Codacy)

Unused variable 'mock_stderr'

Unused variable 'mock_stderr'
main.check_env_permissions(str(env_file))

# Verify success message (or at least no warning about symlink)
# Note: Depending on implementation, it might log "Fixed .env permissions"
# We can check permissions directly.

# Verify permissions are fixed to 600
final_mode = env_file.stat().st_mode
assert (final_mode & 0o777) == 0o600

Check notice

Code scanning / Bandit (reported by Codacy)

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

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

Choose a reason for hiding this comment

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

high

This test ensures that check_env_permissions correctly applies the secure 600 permissions to regular files, complementing the symlink handling test.

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.
Loading