macOS无障碍自动化Skill macos-accessibility

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

操作系统 0 次安装 0 次浏览 更新于 3/15/2026

name: macos-accessibility risk_level: MEDIUM description: “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