check_app_permissions

check_app_permissions

Checks what system permissions an application has been granted (Full Disk Access, Accessibility, Camera, Microphone, etc.). Use before reinstalling software or when an app reports permission errors.

Metadata

Name

check_app_permissions

Updated

2 months ago

Source

GitHub source

Risk

Low

Requires consent

false

Affected scope

user

Code

/**
 * mcp/skills/checkAppPermissions.ts — check_app_permissions skill
 *
 * Checks what system permissions an application has been granted (Full Disk
 * Access, Accessibility, Camera, Microphone, etc.). Use before reinstalling
 * software or when an app reports permission errors.
 *
 * Platform strategy
 * -----------------
 * darwin  sqlite3 query against ~/Library/Application Support/com.apple.TCC/TCC.db
 * win32   PowerShell registry query HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager
 *
 * Smoke test
 *   npx tsx -r dotenv/config mcp/skills/checkAppPermissions.ts "CrowdStrike Falcon"
 */

import * as os       from "os";
import * as nodePath from "path";
import { exec }      from "child_process";
import { promisify } from "util";
import { z }         from "zod";

import { TCC_CATEGORIES } from "./_shared/tcc";

const execAsync = promisify(exec);

// -- Meta ---------------------------------------------------------------------

export const meta = {
  name: "check_app_permissions",
  description:
    "Checks what system permissions an application has been granted " +
    "(Full Disk Access, Accessibility, Camera, Microphone, etc.). " +
    "Use before reinstalling software or when an app reports permission errors.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    appName: z
      .string()
      .describe("Application name to check (e.g. 'CrowdStrike Falcon', 'Jamf Connect')"),
  },
} as const;

// -- Types --------------------------------------------------------------------

interface PermissionEntry {
  category: string;
  status:   "allowed" | "denied" | "unknown";
}

interface PermissionsResult {
  appName:     string;
  platform:    string;
  permissions: PermissionEntry[];
  error?:      string;
}

// -- PowerShell helper --------------------------------------------------------

async function runPS(script: string): Promise<string> {
  const encoded = Buffer.from(script, "utf16le").toString("base64");
  const { stdout } = await execAsync(
    `powershell.exe -NoProfile -NonInteractive -EncodedCommand ${encoded}`,
    { maxBuffer: 10 * 1024 * 1024 },
  );
  return stdout.trim();
}

// -- darwin implementation ----------------------------------------------------

async function checkAppPermissionsDarwin(appName: string): Promise<PermissionsResult> {
  if (!/^[a-zA-Z0-9 _\-.']+$/.test(appName)) {
    throw new Error(`[check_app_permissions] Invalid appName: ${appName}`);
  }

const tccDb = nodePath.join(
    os.homedir(),
    "Library",
    "Application Support",
    "com.apple.TCC",
    "TCC.db",
  );

const safeAppName = appName.replace(/'/g, "''");
  const query = `SELECT service, client, auth_value FROM access WHERE LOWER(client) LIKE LOWER('%${safeAppName}%') OR LOWER(policy_id) LIKE LOWER('%${safeAppName}%');`;

try {
    const { stdout } = await execAsync(
      `sqlite3 '${tccDb.replace(/'/g, "'\\''")}' "${query}" 2>&1`,
      { maxBuffer: 5 * 1024 * 1024 },
    );

const rows = stdout.trim().split("\n").filter(Boolean);

if (rows.length === 0) {
      return {
        appName,
        platform:    "darwin",
        permissions: [],
        error:       "No TCC entries found for this application. App may not have requested permissions yet, or requires elevated access to read system TCC database.",
      };
    }

const permissions: PermissionEntry[] = rows.map((row) => {
      const parts     = row.split("|");
      const authValue = parseInt(parts[2] ?? "0", 10);
      const status: PermissionEntry["status"] =
        authValue === 2 ? "allowed" :
        authValue === 0 ? "denied"  :
        "unknown";
      return {
        category: TCC_CATEGORIES[parts[0] ?? ""] ?? parts[0],
        status,
      };
    });

return { appName, platform: "darwin", permissions };
  } catch (err) {
    const systemTccDb = "/Library/Application Support/com.apple.TCC/TCC.db";
    try {
      const { stdout } = await execAsync(
        `sqlite3 '${systemTccDb}' "${query}" 2>&1`,
        { maxBuffer: 5 * 1024 * 1024 },
      );
      const rows = stdout.trim().split("\n").filter(Boolean);
      const permissions: PermissionEntry[] = rows.map((row) => {
        const parts     = row.split("|");
        const authValue = parseInt(parts[2] ?? "0", 10);
        const status: PermissionEntry["status"] =
          authValue === 2 ? "allowed" :
          authValue === 0 ? "denied"  :
          "unknown";
        return { category: TCC_CATEGORIES[parts[0] ?? ""] ?? parts[0], status };
      });
      return { appName, platform: "darwin", permissions };
    } catch {
      return {
        appName,
        platform:    "darwin",
        permissions: [],
        error:       `Could not read TCC database: ${(err as Error).message}. Full Disk Access may be required.`,
      };
    }
  }
}

// -- win32 implementation -----------------------------------------------------

const WIN_CAPABILITIES = [
  "webcam",
  "microphone",
  "location",
  "contacts",
  "calendar",
  "phoneCall",
  "email",
  "appointments",
  "userAccountInformation",
  "radios",
  "bluetoothSync",
  "appDiagnostics",
  "gazeInput",
  "broadFileSystemAccess",
];

async function checkAppPermissionsWin32(appName: string): Promise<PermissionsResult> {
  if (!/^[a-zA-Z0-9 _\-.']+$/.test(appName)) {
    throw new Error(`[check_app_permissions] Invalid appName: ${appName}`);
  }

const safeApp = appName.replace(/'/g, "''");
  const caps    = WIN_CAPABILITIES.map((c) => `'${c}'`).join(",");

const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$basePath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\CapabilityAccessManager\ConsentStore'
$results = @()
foreach ($cap in @(${caps})) {
  $capPath = "$basePath\$cap"
  if (Test-Path $capPath) {
    $apps = Get-ChildItem -Path $capPath -ErrorAction SilentlyContinue
    foreach ($app in $apps) {
      if ($app.PSChildName -like "*${safeApp}*") {
        $val = (Get-ItemProperty -Path $app.PSPath -Name 'Value' -ErrorAction SilentlyContinue).Value
        $results += [PSCustomObject]@{
          category = $cap
          status   = if ($val -eq 'Allow') { 'allowed' } elseif ($val -eq 'Deny') { 'denied' } else { 'unknown' }
        }
      }
    }
  }
}
$results | ConvertTo-Json -Depth 2 -Compress`.trim();

try {
    const raw = await runPS(ps);
    if (!raw || raw === "null") {
      return { appName, platform: "win32", permissions: [], error: "No capability entries found for this application." };
    }
    const parsed      = JSON.parse(raw) as PermissionEntry | PermissionEntry[];
    const permissions = Array.isArray(parsed) ? parsed : [parsed];
    return { appName, platform: "win32", permissions };
  } catch (err) {
    return {
      appName,
      platform:    "win32",
      permissions: [],
      error:       (err as Error).message,
    };
  }
}

// -- Exported run function ----------------------------------------------------

export async function run({ appName }: { appName: string }) {
  const platform = os.platform();
  return platform === "win32"
    ? checkAppPermissionsWin32(appName)
    : checkAppPermissionsDarwin(appName);
}

// -- CLI smoke test -----------------------------------------------------------

if (false) {
  run({ appName: process.argv[2] ?? "Slack" })
    .then((r) => console.log(JSON.stringify(r, null, 2)))
    .catch((err: Error) => { console.error(err.message); process.exit(1); });
}