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

186 lines
7.4 KiB
Python

"""Entitlement-style inspection, Lynis hook, SwiftBelt-inspired paths."""
from __future__ import annotations
from pathlib import Path
from applepy.context import RunContext
from applepy.findings import Finding, Severity
from applepy.registry import CheckRegistry
from applepy.subproc import run_text
def register(registry: CheckRegistry) -> None:
registry.register("surf_entitlements", check_entitlements_sample, phases=("unprivileged",))
registry.register("surf_swiftbeltish", check_collaboration_paths, phases=("unprivileged",))
registry.register("surf_ssh", check_ssh_dir, phases=("unprivileged",))
registry.register("surf_tcc_loginitems", check_tcc_and_loginitem_stores, phases=("unprivileged",))
def check_entitlements_sample(ctx: RunContext) -> list[Finding]:
apps = Path("/Applications")
if not apps.is_dir():
return [
Finding(
id="ent-001",
title="Applications folder not readable",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description="Cannot enumerate /Applications for codesign entitlements sampling.",
evidence="/Applications missing or not a directory",
worksheet="Entitlements",
mitre_techniques=("T1552",),
)
]
app_bundles = sorted(p for p in apps.iterdir() if p.suffix == ".app")
lines: list[str] = []
for app in app_bundles:
exe = app / "Contents" / "MacOS" / app.stem
if not exe.is_file():
exes = list((app / "Contents" / "MacOS").glob("*")) if (app / "Contents" / "MacOS").is_dir() else []
exe = exes[0] if exes else None
if not exe or not exe.is_file():
lines.append(f"{app.name}: (no Mach-O sampled)")
continue
code, out, err = run_text(["/usr/bin/codesign", "-d", "--entitlements", ":-", str(exe)], timeout=20)
blob = (out + err).strip()
lines.append(f"{app.name} ({exe.name}): exit={code}\n{blob}")
return [
Finding(
id="ent-002",
title="Sample application entitlements (codesign)",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description=(
"Every `*.app` bundle directly under `/Applications` — EntitlementCheck-style `codesign` "
f"entitlements dump per sampled Mach-O (total bundles enumerated: {len(app_bundles)})."
),
evidence="\n\n".join(lines),
worksheet="Entitlements",
mitre_techniques=("T1552.002", "T1626"),
risk="Over-privileged entitlements weaken sandbox and TCC expectations.",
impact="Malware or RATs may abuse accessibility, Apple Events, or debugging entitlements.",
remediation="Compare entitlements to vendor documentation; remove unnecessary hardened runtime exceptions.",
references=("https://github.com/cedowens/EntitlementCheck",),
)
]
def check_collaboration_paths(ctx: RunContext) -> list[Finding]:
home = ctx.home
paths = [
home / "Library" / "Application Support" / "Slack",
home / "Library" / "Application Support" / "Microsoft" / "Teams",
home / "Library" / "Containers" / "com.tinyspeck.slackmacgap",
home / "Library" / "Keychains",
]
lines = [f"{p}: {'exists' if p.exists() else 'absent'}" for p in paths]
return [
Finding(
id="surf-001",
title="Collaboration and keychain-adjacent paths",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description="SwiftBelt-style interest paths for credential and cache artefacts (presence only).",
evidence="\n".join(lines),
worksheet="Attack surface",
mitre_techniques=("T1539", "T1555"),
)
]
def check_tcc_and_loginitem_stores(ctx: RunContext) -> list[Finding]:
"""Presence-only paths for TCC, classic login items, and background task management (no DB reads)."""
home = ctx.home
rows: list[str] = []
checks: list[tuple[str, Path, str]] = [
(
"User TCC database",
home / "Library" / "Application Support" / "com.apple.TCC" / "tcc.db",
"file",
),
(
"System TCC database",
Path("/Library/Application Support/com.apple.TCC/tcc.db"),
"file",
),
(
"Login Items plist",
home / "Library" / "Preferences" / "com.apple.loginitems.plist",
"file",
),
(
"Background Task Management support",
home / "Library" / "Application Support" / "com.apple.backgroundtaskmanagementagent",
"dir",
),
]
for label, path, kind in checks:
try:
if kind == "file":
if path.is_file():
st = path.stat()
rows.append(f"{label}: present file {path} ({st.st_size} bytes)")
else:
rows.append(f"{label}: absent or not a file ({path})")
elif path.is_dir():
rows.append(f"{label}: directory exists {path}")
else:
rows.append(f"{label}: absent ({path})")
except OSError as e:
rows.append(f"{label}: {path} ({e})")
return [
Finding(
id="surf-004",
title="TCC, login items, and background task stores (presence only)",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description=(
"Standard paths for Transparency Consent and Control (user), system TCC store, legacy "
"login-items plist, and Background Task Management support data. Sizes and presence only — "
"databases are not opened."
),
evidence="\n".join(rows),
worksheet="Attack surface",
mitre_techniques=("T1012", "T1543.001"),
risk="These stores govern privacy prompts and login-time execution; tampering affects consent UX.",
impact="Malware may target TCC or login persistence; reviewers should correlate with MDM telemetry.",
remediation="Review login items in System Settings; validate TCC decisions against policy.",
references=(
"https://support.kandji.io/kb/configure-the-login-background-items-library-item",
),
)
]
def check_ssh_dir(ctx: RunContext) -> list[Finding]:
d = ctx.home / ".ssh"
if not d.is_dir():
return [
Finding(
id="surf-002",
title="SSH directory",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description="~/.ssh not present or not a directory.",
evidence=str(d),
worksheet="Attack surface",
mitre_techniques=("T1552.004",),
)
]
names = sorted(p.name for p in d.iterdir() if p.is_file())
return [
Finding(
id="surf-003",
title="SSH directory file names",
category="Attack surface",
severity=Severity.INFORMATIONAL,
description="File names only — private key material is not read.",
evidence="\n".join(names),
worksheet="Attack surface",
mitre_techniques=("T1552.004",),
)
]