Files
applepy/applepy/checks/fs_posture.py
Warezpeddler 3325436017 Initial commit
2026-04-25 23:09:31 +01:00

165 lines
6.4 KiB
Python

"""Filesystem hardening signals (world-writable files in hook directories)."""
from __future__ import annotations
import os
import stat
from pathlib import Path
from applepy.context import RunContext
from applepy.findings import Finding, Severity
from applepy.registry import CheckRegistry
def _env_positive_int(name: str, default: int) -> int:
raw = os.environ.get(name, "").strip()
if not raw:
return default
try:
v = int(raw)
return v if v > 0 else default
except ValueError:
return default
# Override with APPLEPY_FS_MAX_SCAN / APPLEPY_FS_MAX_HITS if a host is very large.
_DEFAULT_MAX_SCAN = _env_positive_int("APPLEPY_FS_MAX_SCAN", 500_000)
_DEFAULT_MAX_HITS = _env_positive_int("APPLEPY_FS_MAX_HITS", 100_000)
def collect_world_writable_files(
roots: list[Path],
*,
max_scan: int = _DEFAULT_MAX_SCAN,
max_hits: int = _DEFAULT_MAX_HITS,
) -> tuple[list[str], list[str]]:
"""
Return (hit_lines, notes). Each hit is 'path mode=0o....'.
Stops after max_scan file stats or max_hits world-writable regular files (see env vars above).
"""
hits: list[str] = []
notes: list[str] = []
scanned = 0
for root in roots:
if not root.is_dir():
continue
try:
for dirpath, _dirnames, filenames in os.walk(
root,
topdown=True,
followlinks=False,
):
for fn in filenames:
if scanned >= max_scan or len(hits) >= max_hits:
if len(hits) >= max_hits:
notes.append(
f"Hit cap reached ({max_hits} world-writable regular files) "
f"(APPLEPY_FS_MAX_HITS); scanned {scanned} entries — increase the variable if more "
"matches must be listed."
)
if scanned >= max_scan:
notes.append(
f"Scan stopped after examining {max_scan} file system entries "
f"(APPLEPY_FS_MAX_SCAN); {len(hits)} world-writable regular files recorded — "
"increase the variable for a complete pass on very large trees."
)
return hits, notes
path = Path(dirpath) / fn
try:
st = path.lstat()
except OSError:
continue
scanned += 1
if not stat.S_ISREG(st.st_mode):
continue
if st.st_mode & stat.S_IWOTH:
mode_oct = oct(st.st_mode & 0o777)
hits.append(f"{path} mode={mode_oct}")
except OSError as e:
notes.append(f"{root}: walk failed ({e})")
return hits, notes
def register(registry: CheckRegistry) -> None:
registry.register(
"fs_world_writable_user",
check_world_writable_user_launch_dirs,
phases=("unprivileged",),
)
registry.register(
"fs_world_writable_system",
check_world_writable_system_launch_dirs,
phases=("privileged",),
)
def check_world_writable_user_launch_dirs(ctx: RunContext) -> list[Finding]:
roots = [
ctx.home / "Library" / "LaunchAgents",
ctx.home / "Library" / "LaunchDaemons",
]
hits, notes = collect_world_writable_files(roots)
ev_parts = list(hits)
ev_parts.extend(notes)
evidence = "\n".join(ev_parts) if ev_parts else "(no world-writable regular files under user Launch* dirs)"
sev = Severity.HIGH if hits else Severity.INFORMATIONAL
return [
Finding(
id="fs-001",
title="World-writable files under user LaunchAgents / LaunchDaemons",
category="Hardening",
severity=sev,
description=(
"Regular files writable by others under per-user launch directories can allow local "
"privilege abuse or persistence tampering; aligns with library/system folder reviews "
"recommended in enterprise macOS baselines."
),
evidence=evidence,
worksheet="Hardening",
mitre_techniques=("T1222", "T1543.001", "T1543.004"),
risk="World-writable launch artefacts weaken integrity expectations for login-time execution.",
impact="Low-privilege users or malware may alter plist payloads or helper scripts.",
remediation="Remove world-writable bits; ensure ownership is the user and group is staff (or equivalent).",
references=(
"https://support.kandji.io/kb/checking-library-and-system-folders-for-world-writable-files",
),
)
]
def check_world_writable_system_launch_dirs(ctx: RunContext) -> list[Finding]:
if not ctx.is_root():
return []
roots = [
Path("/Library/LaunchDaemons"),
Path("/Library/LaunchAgents"),
]
hits, notes = collect_world_writable_files(roots)
ev_parts = list(hits)
ev_parts.extend(notes)
evidence = "\n".join(ev_parts) if ev_parts else "(no world-writable regular files under /Library Launch* dirs)"
sev = Severity.CRITICAL if hits else Severity.INFORMATIONAL
return [
Finding(
id="fs-002",
title="World-writable files under system LaunchDaemons / LaunchAgents",
category="Hardening",
severity=sev,
description=(
"World-writable plists or binaries under /Library/Launch* are a serious integrity failure "
"on macOS; scan is depth-first with configurable file and hit caps (see APPLEPY_FS_MAX_*)."
),
evidence=evidence,
worksheet="Hardening",
mitre_techniques=("T1222", "T1543.001", "T1543.004"),
risk="Any local process may alter launch configuration consumed at boot or login.",
impact="Persistence, privilege escalation, or denial of service across all users.",
remediation="Immediately audit ownership and modes; restore vendor defaults and investigate root cause.",
references=(
"https://support.kandji.io/kb/checking-library-and-system-folders-for-world-writable-files",
),
)
]