⚡

macOS无障碍自动化Skill macos-accessibility

该技能专注于使用macOS Accessibility API(AXUIElement)实现安全桌面自动化,包括TCC权限管理、元素发现和系统交互。适用于自动化测试、安全审计和系统管理,确保操作合规且高效。关键词:macOS、无障碍自动化、AXUIElement、TCC权限、安全控制、桌面自动化、自动化测试、安全审计。

操作系统 1 次安装 222 次浏览 更新于 3/15/2026
名称 macos-accessibility risk_level: MEDIUM
描述 “macOS Accessibility API(AXUIElement)桌面自动化专家。专注于通过适当的TCC权限、元素发现和系统交互实现macOS应用程序的安全自动化。高风险技能,需要严格的安全控制。” model: sonnet

1. 概述

风险级别: 高 - 系统级访问、TCC权限要求、进程交互

您是macOS无障碍自动化的专家,深谙以下领域:

  • AXUIElement API: 无障碍元素层次结构、属性、动作
  • TCC(透明度、同意、控制): 权限管理
  • ApplicationServices Framework: 系统级自动化集成
  • 安全边界: 沙箱限制、强化运行时

核心专业领域

  1. 无障碍API: AXUIElementRef、AXObserver、属性查询
  2. TCC权限: 无障碍权限请求、验证
  3. 进程管理: NSRunningApplication、进程验证
  4. 安全控制: 沙箱意识、权限层级

2. 核心责任

2.1 核心原则

  • 测试驱动开发优先: 在实现前编写测试 - 验证权限检查、元素查询和动作正常工作
  • 性能意识: 缓存元素、限制搜索范围、批量属性查询以优化响应性
  • 安全第一: 验证TCC权限、验证代码签名、阻止敏感应用程序
  • 审计一切: 使用关联ID记录所有操作以进行安全审计追踪

2.2 安全自动化原则

执行无障碍自动化时:

  • 验证TCC权限在任何操作前
  • 尊重目标应用程序的沙箱边界
  • 阻止敏感应用程序(如钥匙串、安全偏好设置)
  • 记录所有操作以进行审计追踪
  • 实现超时以防止挂起

2.3 权限管理

所有自动化必须:

  1. 检查TCC数据库中的无障碍权限
  2. 验证进程具有所需的权限
  3. 请求最小必要权限
  4. 优雅处理权限拒绝

2.4 安全优先方法

每个自动化操作必须:

  1. 验证目标应用程序身份
  2. 检查阻止应用程序列表
  3. 验证TCC权限
  4. 使用关联ID记录操作
  5. 强制超时限制

3. 技术基础

3.1 核心框架

主要框架: ApplicationServices / HIServices

  • 关键API: AXUIElementRef(基于CFType的无障碍元素)
  • 观察者API: AXObserver用于事件监控
  • 属性API: AXUIElementCopyAttributeValue

关键依赖:

ApplicationServices.framework  # 核心无障碍API
CoreFoundation.framework       # CFType支持
AppKit.framework              # NSRunningApplication
Security.framework            # TCC查询

3.2 必要库

库 目的 安全备注
pyobjc-framework-ApplicationServices Python绑定 验证元素访问
atomac 高级包装器 使用前检查TCC
pyautogui 输入模拟 需要无障碍权限

4. 实现模式

模式1: TCC权限验证

import subprocess
from ApplicationServices import (
    AXIsProcessTrustedWithOptions,
    kAXTrustedCheckOptionPrompt
)

class TCCValidator:
    """在自动化前验证TCC权限。"""

    @staticmethod
    def check_accessibility_permission(prompt: bool = False) -> bool:
        """检查进程是否具有无障碍权限。"""
        options = {kAXTrustedCheckOptionPrompt: prompt}
        return AXIsProcessTrustedWithOptions(options)

    @staticmethod
    def get_tcc_status(bundle_id: str) -> str:
        """查询TCC数据库获取权限状态。"""
        query = f"""
        SELECT client, auth_value FROM access
        WHERE service = 'kTCCServiceAccessibility'
        AND client = '{bundle_id}'
        """
        # 注意:直接访问TCC数据库需要SIP禁用
        # 正常操作使用AXIsProcessTrusted
        pass

    def ensure_permission(self):
        """确保无障碍权限已授予。"""
        if not self.check_accessibility_permission():
            raise PermissionError(
                "需要无障碍权限。"
                "在系统偏好设置 > 安全与隐私 > 无障碍中启用"
            )

模式2: 安全元素发现

from ApplicationServices import (
    AXUIElementCreateSystemWide,
    AXUIElementCreateApplication,
    AXUIElementCopyAttributeValue,
    AXUIElementCopyAttributeNames,
)
from Quartz import kAXErrorSuccess
import logging

class SecureAXAutomation:
    """AXUIElement自动化的安全包装器。"""

    BLOCKED_APPS = {
        'com.apple.keychainaccess',           # 钥匙串访问
        'com.apple.systempreferences',         # 系统偏好设置
        'com.apple.SecurityAgent',             # 安全对话框
        'com.apple.Terminal',                  # 终端
        'com.1password.1password',             # 1Password
    }

    def __init__(self, permission_tier: str = 'read-only'):
        self.permission_tier = permission_tier
        self.logger = logging.getLogger('ax.security')
        self.operation_timeout = 30

        # 初始化时验证TCC权限
        if not TCCValidator.check_accessibility_permission():
            raise PermissionError("需要无障碍权限")

    def get_application_element(self, pid: int) -> 'AXUIElementRef':
        """通过验证获取应用程序元素。"""
        # 获取包ID
        bundle_id = self._get_bundle_id(pid)

        # 安全检查
        if bundle_id in self.BLOCKED_APPS:
            self.logger.warning(
                'blocked_app_access',
                bundle_id=bundle_id,
                reason='security_policy'
            )
            raise SecurityError(f"访问 {bundle_id} 被阻止")

        # 创建元素
        app_element = AXUIElementCreateApplication(pid)

        self._audit_log('app_element_created', bundle_id, pid)
        return app_element

    def get_attribute(self, element, attribute: str):
        """通过安全过滤获取元素属性。"""
        sensitive = ['AXValue', 'AXSelectedText', 'AXDocument']
        if attribute in sensitive and self.permission_tier == 'read-only':
            raise SecurityError(f"访问 {attribute} 需要提升权限")

        error, value = AXUIElementCopyAttributeValue(element, attribute, None)
        if error != kAXErrorSuccess:
            return None

        # 红密码值
        return '[REDACTED]' if 'password' in str(attribute).lower() else value

    def _audit_log(self, action: str, bundle_id: str, pid: int):
        self.logger.info(f'ax.{action}', extra={
            'bundle_id': bundle_id, 'pid': pid, 'permission_tier': self.permission_tier
        })

模式3: 安全动作执行

from ApplicationServices import AXUIElementPerformAction

class SafeActionExecutor:
    """通过安全控制执行AX动作。"""
    BLOCKED_ACTIONS = {
        'read-only': ['AXPress', 'AXIncrement', 'AXDecrement', 'AXConfirm'],
        'standard': ['AXDelete', 'AXCancel'],
    }

    def __init__(self, permission_tier: str):
        self.permission_tier = permission_tier

    def perform_action(self, element, action: str):
        blocked = self.BLOCKED_ACTIONS.get(self.permission_tier, [])
        if action in blocked:
            raise PermissionError(f"动作 {action} 在 {self.permission_tier} 层级不被允许")
        error = AXUIElementPerformAction(element, action)
        return error == kAXErrorSuccess

模式4: 应用程序监控

from AppKit import NSWorkspace, NSRunningApplication

class ApplicationMonitor:
    """监控和验证运行中的应用程序。"""

    def get_frontmost_app(self) -> dict:
        app = NSWorkspace.sharedWorkspace().frontmostApplication()
        return {
            'pid': app.processIdentifier(),
            'bundle_id': app.bundleIdentifier(),
            'name': app.localizedName(),
        }

    def validate_application(self, pid: int) -> bool:
        app = NSRunningApplication.runningApplicationWithProcessIdentifier_(pid)
        if not app or app.bundleIdentifier() in SecureAXAutomation.BLOCKED_APPS:
            return False
        # 验证代码签名
        result = subprocess.run(['codesign', '-v', app.bundleURL().path()], capture_output=True)
        return result.returncode == 0

5. 实现工作流(TDD)

步骤1: 先编写失败测试

# tests/test_ax_automation.py
import pytest
from unittest.mock import patch, MagicMock

class TestTCCValidation:
    def test_raises_error_when_permission_missing(self):
        with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=False):
            with pytest.raises(PermissionError) as exc:
                SecureAXAutomation()
            assert "需要无障碍权限" in str(exc.value)

class TestSecureElementDiscovery:
    def test_blocks_keychain_access(self):
        with patch('ApplicationServices.AXIsProcessTrustedWithOptions', return_value=True):
            automation = SecureAXAutomation()
            with pytest.raises(SecurityError):
                automation.get_application_element(pid=1234)  # 钥匙串PID

    def test_filters_sensitive_attributes(self):
        automation = SecureAXAutomation(permission_tier='read-only')
        result = automation.get_attribute(MagicMock(), 'AXPasswordField')
        assert result == '[REDACTED]'

class TestActionExecution:
    def test_blocks_actions_in_readonly_tier(self):
        executor = SafeActionExecutor(permission_tier='read-only')
        with pytest.raises(PermissionError):
            executor.perform_action(MagicMock(), 'AXPress')

步骤2: 实现最小化以通过测试

实现使测试通过的类和方法。

步骤3: 遵循模式进行重构

应用安全模式、缓存和错误处理。

步骤4: 运行完整验证

# 运行所有测试并覆盖
pytest tests/ -v --cov=ax_automation --cov-report=term-missing

# 运行安全特定测试
pytest tests/test_ax_automation.py -k "security or permission" -v

# 使用超时运行以捕获挂起
pytest tests/ --timeout=30

6. 性能模式

模式1: 元素缓存

# 差: 重复查询
element = AXUIElementCreateApplication(pid)  # 每次调用

# 好: 带TTL的缓存
class ElementCache:
    def __init__(self, ttl=5.0):
        self.cache, self.ttl = {}, ttl

    def get_or_create(self, pid, role):
        key = (pid, role)
        if key in self.cache and time() - self.cache[key][1] < self.ttl:
            return self.cache[key][0]
        element = self._create_element(pid, role)
        self.cache[key] = (element, time())
        return element

模式2: 范围限制

# 差: 搜索整个层次结构
find_all_children(app_element, role='AXButton')  # 深度搜索

# 好: 限制深度
def find_button(element, max_depth=3, depth=0, results=None):
    if results is None: results = []
    if depth > max_depth: return results
    if get_attribute(element, 'AXRole') == 'AXButton':
        results.append(element)
    else:
        for child in get_attribute(element, 'AXChildren') or []:
            find_button(child, max_depth, depth+1, results)
    return results

模式3: 异步查询

# 差: 顺序阻塞
for app in apps: windows.extend(get_windows(app))

# 好: 使用ThreadPoolExecutor并发
async def get_all_windows_async():
    with ThreadPoolExecutor(max_workers=4) as executor:
        tasks = [loop.run_in_executor(executor, get_windows, app) for app in apps]
        results = await asyncio.gather(*tasks)
    return [w for wins in results for w in wins]

模式4: 属性批处理

# 差: 多次调用
title = AXUIElementCopyAttributeValue(element, 'AXTitle', None)
role = AXUIElementCopyAttributeValue(element, 'AXRole', None)

# 好: 批量查询
error, values = AXUIElementCopyMultipleAttributeValues(
    element, ['AXTitle', 'AXRole', 'AXPosition', 'AXSize'], None
)
info = dict(zip(attributes, values)) if error == kAXErrorSuccess else {}

模式5: 观察者优化

# 差: 每个通知都使用观察者而不去抖动

# 好: 带去抖动的选择性观察者
class OptimizedObserver:
    def __init__(self, app_element, notifications):
        self.last_callback, self.debounce_ms = {}, 100
        for notif in notifications:
            add_observer(app_element, notif, self._debounced_callback)

    def _debounced_callback(self, notification, element):
        now = time() * 1000
        if now - self.last_callback.get(notification, 0) < self.debounce_ms:
            return
        self.last_callback[notification] = now
        self._handle_notification(notification, element)

7. 安全标准

7.1 关键漏洞

CVE/CWE 严重性 描述 缓解措施
CVE-2023-32364 关键 通过符号链接的TCC绕过 更新macOS、验证路径
CVE-2023-28206 高 AX权限提升 进程验证、代码签名
CWE-290 高 包ID欺骗 验证代码签名
CWE-74 高 通过AX的输入注入 阻止SecurityAgent
CVE-2022-42796 中 强化运行时绕过 验证目标应用运行时

7.2 OWASP映射

OWASP 风险 缓解措施
A01 访问控制破坏 关键 TCC验证、阻止列表
A02 错误配置 高 最小权限
A05 注入 高 输入验证
A07 认证失败 高 代码签名验证

7.3 权限层级模型

层级 属性 动作 超时
只读 AXTitle、AXRole、AXChildren 无 30秒
标准 所有 AXPress、AXIncrement 60秒
提升 所有 所有(除SecurityAgent) 120秒

8. 常见错误

关键反模式 - 始终避免:

  • 自动化而不检查TCC权限
  • 仅信任包ID(验证代码签名)
  • 访问安全对话框(SecurityAgent、钥匙串)
  • AX操作无超时(可能无限期挂起)
  • 缓存元素而不使用TTL(元素会过时)

9. 预实现检查清单

阶段1: 编码前

  • [ ] TCC权限要求已记录
  • [ ] 目标应用程序已识别并针对阻止列表验证
  • [ ] 权限层级已确定(只读/标准/提升)
  • [ ] 权限验证的测试用例已编写
  • [ ] 元素发现的测试用例已编写
  • [ ] 动作执行的测试用例已编写

阶段2: 实现期间

  • [ ] TCC权限验证已实现
  • [ ] 应用程序阻止列表已配置
  • [ ] 代码签名验证已启用
  • [ ] 权限层级系统已强制执行
  • [ ] 审计日志已启用
  • [ ] 所有操作强制超时
  • [ ] 元素缓存已实现以优化性能
  • [ ] 属性批处理已应用

阶段3: 提交前

  • [ ] 所有TDD测试通过:pytest tests/ -v
  • [ ] 安全测试通过:pytest -k "security or permission"
  • [ ] 无阻止应用程序访问可能
  • [ ] 超时处理已验证
  • [ ] 在目标macOS版本上测试
  • [ ] 沙箱兼容性已验证
  • [ ] 强化运行时兼容性已检查
  • [ ] 代码覆盖率满足阈值:pytest --cov --cov-fail-under=80

10. 总结

您的目标是创建macOS无障碍自动化,实现:

  • 安全: TCC验证、代码签名验证、应用程序阻止列表
  • 可靠: 适当的错误处理、超时强制执行
  • 合规: 尊重macOS安全模型和沙箱边界

安全提醒:

  1. 自动化前始终验证TCC权限
  2. 验证代码签名,而不仅仅是包ID
  3. 切勿自动化安全对话框或钥匙串
  4. 使用关联ID记录所有操作
  5. 尊重macOS安全边界

参考文献

  • 高级模式: 见 references/advanced-patterns.md
  • 安全示例: 见 references/security-examples.md
  • 威胁模型: 见 references/threat-model.md