Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
b9d6fff
fix(output): clear inherited report ACLs before writing
yashrajp22 Oct 21, 2026
354035b
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 22, 2026
666223d
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 22, 2026
5359647
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 22, 2026
2d318a3
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 22, 2026
27c2cb3
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 27, 2026
6baf0e8
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 27, 2026
972639c
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 27, 2026
bae2dc3
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
0e9f979
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
996e340
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
b91d24c
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
5822f9a
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
4496ef3
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
61b9fc3
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
347d21b
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
02eaf2a
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
490b77b
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
030fac2
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
c721f3b
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
686bc58
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
5402ced
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 28, 2026
cf90fea
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
911c391
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
34df10f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
09e1849
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
006334c
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
b285ea7
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 29, 2026
b567a39
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 30, 2026
84d8fb0
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 30, 2026
0ef740e
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 30, 2026
036f907
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 31, 2026
736875f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 31, 2026
3a3b8a1
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 31, 2026
36839c7
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 31, 2026
0ad81d0
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Oct 31, 2026
0d3f89f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
fed2bf7
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
b69dd43
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
0a2c202
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
391c710
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
3bcc109
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
8b02a51
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
33e3d3f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
38b2033
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
03848a8
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
fbc0f39
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
4605c27
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
2fe9bc9
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
bd92b73
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
8aba964
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 2, 2026
361de7e
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 3, 2026
b05af9c
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 3, 2026
8468934
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
c2a4510
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
1278047
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
10eeb0f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
7d5c47f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
7da927d
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
c5b460e
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
3c3afe6
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 4, 2026
b83dc31
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 5, 2026
dddb6bf
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 5, 2026
04ac7f6
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 5, 2026
3966181
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
7eae31f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
9b20188
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
b0787b6
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
afffeef
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
43d3052
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
fc79a94
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 6, 2026
cbf25c9
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
bcf505e
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
ba9f188
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
0e7ba57
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
1d21756
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
d57696f
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
d8eacbf
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
4f1ff50
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
857b1d1
Merge branch 'main' into yashraj/fix-report-inherited-acls
github-actions[bot] Nov 9, 2026
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
40 changes: 40 additions & 0 deletions src/skillspector/file_output.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@

import errno
import os
import sys
from collections.abc import Iterator
from contextlib import contextmanager
from pathlib import Path
Expand Down Expand Up @@ -46,6 +47,44 @@ def require_secure_file_output() -> None:
)


def restrict_temporary_access(descriptor: int, *, output_kind: str) -> None:
"""Remove inherited access before a temporary file receives output data."""
if os.name == "posix":
# Also masks named-user/group ACL grants on POSIX ACL implementations.
os.fchmod(descriptor, 0o600)
if sys.platform != "darwin":
return

# macOS extended ACL grants are independent of permission bits. Use the
# already-open descriptor so clearing them cannot follow a swapped path.
import ctypes

try:
libc = ctypes.CDLL(None, use_errno=True)
init_acl = libc.acl_init
set_acl = libc.acl_set_fd_np
free_acl = libc.acl_free
except AttributeError:
# Filesystems/platforms without extended ACL support need only mode bits.
return
init_acl.argtypes = [ctypes.c_int]
init_acl.restype = ctypes.c_void_p
set_acl.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
set_acl.restype = ctypes.c_int
free_acl.argtypes = [ctypes.c_void_p]
free_acl.restype = ctypes.c_int
empty_acl = init_acl(0)
if not empty_acl:
raise OSError(ctypes.get_errno(), f"Could not initialize {output_kind} ACL")
try:
if set_acl(descriptor, empty_acl, 0x100) != 0: # ACL_TYPE_EXTENDED
error = ctypes.get_errno()
if error not in {errno.ENOTSUP, errno.EOPNOTSUPP}:
raise OSError(error, f"Could not clear inherited {output_kind} ACLs")
finally:
free_acl(empty_acl)


@contextmanager
def open_output_parent(path: str | Path) -> Iterator[tuple[Path, int]]:
"""Keep a no-follow parent descriptor open for all operations on an output."""
Expand Down Expand Up @@ -118,6 +157,7 @@ def write_text_no_follow(path: str | Path, text: str) -> None:
)
temporary_name = candidate
with os.fdopen(fd, "w", encoding="utf-8") as stream:
restrict_temporary_access(stream.fileno(), output_kind="report")
stream.write(text)
os.replace(
temporary_name,
Expand Down
37 changes: 2 additions & 35 deletions src/skillspector/suppression.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@

import yaml

from skillspector.file_output import open_output_parent
from skillspector.file_output import open_output_parent, restrict_temporary_access
from skillspector.logging_config import get_logger
from skillspector.models import Finding

Expand Down Expand Up @@ -656,40 +656,7 @@ def build_baseline_dict(

def _restrict_baseline_temporary(descriptor: int) -> None:
"""Remove inherited access before a temporary file receives baseline data."""
if os.name == "posix":
# Also masks named-user/group ACL grants on POSIX ACL implementations.
os.fchmod(descriptor, 0o600)
if sys.platform != "darwin":
return

# macOS extended ACL grants are independent of permission bits. Use the
# already-open descriptor so clearing them cannot follow a swapped path.
import ctypes

try:
libc = ctypes.CDLL(None, use_errno=True)
init_acl = libc.acl_init
set_acl = libc.acl_set_fd_np
free_acl = libc.acl_free
except AttributeError:
# Filesystems/platforms without extended ACL support need only mode bits.
return
init_acl.argtypes = [ctypes.c_int]
init_acl.restype = ctypes.c_void_p
set_acl.argtypes = [ctypes.c_int, ctypes.c_void_p, ctypes.c_int]
set_acl.restype = ctypes.c_int
free_acl.argtypes = [ctypes.c_void_p]
free_acl.restype = ctypes.c_int
empty_acl = init_acl(0)
if not empty_acl:
raise OSError(ctypes.get_errno(), "Could not initialize baseline ACL")
try:
if set_acl(descriptor, empty_acl, 0x100) != 0: # ACL_TYPE_EXTENDED
error = ctypes.get_errno()
if error not in {errno.ENOTSUP, errno.EOPNOTSUPP}:
raise OSError(error, "Could not clear inherited baseline ACLs")
finally:
free_acl(empty_acl)
restrict_temporary_access(descriptor, output_kind="baseline")


def _preserve_baseline_acl(source: int, destination: int) -> None:
Expand Down
90 changes: 90 additions & 0 deletions tests/unit/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -7977,6 +7977,96 @@ def fail_replace(*args, **kwargs):
assert not list(tmp_path.glob(".skillspector-output-*"))


@pytest.mark.skipif(os.name != "posix", reason="requires POSIX descriptors")
@pytest.mark.parametrize("existing", [False, True])
def test_report_output_restricts_access_before_writing(tmp_path, monkeypatch, existing):
from skillspector import file_output

output = tmp_path / "report.json"
if existing:
output.write_text("previous", encoding="utf-8")
original_restrict = file_output.restrict_temporary_access
target_sizes = []

def restrict_empty_file(descriptor, *, output_kind):
target_sizes.append(os.fstat(descriptor).st_size)
original_restrict(descriptor, output_kind=output_kind)

monkeypatch.setattr(file_output, "restrict_temporary_access", restrict_empty_file)
file_output.write_text_no_follow(output, "private report")

assert target_sizes == [0]
assert output.read_text(encoding="utf-8") == "private report"
assert output.stat().st_mode & 0o777 == 0o600
assert not list(tmp_path.glob(".skillspector-output-*"))


@pytest.mark.skipif(os.name != "posix", reason="requires POSIX descriptors")
@pytest.mark.parametrize("existing", [False, True])
def test_report_output_preserves_destination_when_acl_removal_fails(
tmp_path, monkeypatch, existing
):
import ctypes
import errno

from skillspector import file_output

output = tmp_path / "report.json"
if existing:
output.write_text("previous", encoding="utf-8")
target_sizes = []

def fail_set_acl(descriptor, acl, acl_type):
target_sizes.append(os.fstat(descriptor).st_size)
ctypes.set_errno(errno.EACCES)
return -1

monkeypatch.setattr(sys, "platform", "darwin")
monkeypatch.setattr(
ctypes,
"CDLL",
lambda *args, **kwargs: SimpleNamespace(
acl_init=lambda count: 1, acl_set_fd_np=fail_set_acl, acl_free=lambda acl: 0
),
)
with pytest.raises(ValueError, match="clear inherited report ACLs"):
file_output.write_text_no_follow(output, "private report")

assert target_sizes == [0]
if existing:
assert output.read_text(encoding="utf-8") == "previous"
else:
assert not output.exists()
assert not list(tmp_path.glob(".skillspector-output-*"))


@pytest.mark.skipif(sys.platform != "darwin", reason="requires macOS inherited ACLs")
@pytest.mark.parametrize("mode", [None, 0o600, 0o200])
def test_report_output_clears_inherited_macos_acl(tmp_path, mode):
import subprocess

from skillspector import file_output

output = tmp_path / "report.json"
if mode is not None:
output.write_text("previous", encoding="utf-8")
output.chmod(mode)
subprocess.run(
["/bin/chmod", "+a", "everyone allow read,file_inherit", str(tmp_path)],
check=True,
capture_output=True,
)

file_output.write_text_no_follow(output, "private report")

acl_listing = subprocess.run(
["/bin/ls", "-le", str(output)], check=True, capture_output=True, text=True
).stdout
assert "everyone" not in acl_listing
assert output.stat().st_mode & 0o777 == 0o600
assert output.read_text(encoding="utf-8") == "private report"


def test_report_output_unsupported_platform_keeps_stdout(tmp_path, monkeypatch):
from skillspector import file_output

Expand Down