186 lines
7.4 KiB
Python
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",),
|
|
)
|
|
]
|