get_cached_credentials_count
get_cached_credentials_count
Reports per-domain cached credential counts in macOS Keychain (login) or Windows Credential Manager without modifying anything. Read-only counterpart to purge_cached_credentials.
Metadata
Name
get_cached_credentials_count
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/getCachedCredentialsCount.ts — get_cached_credentials_count
*
* Reports per-domain cached credential counts in macOS Keychain (login)
* or Windows Credential Manager without modifying anything. Read-only
* counterpart to `purge_cached_credentials`.
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/getCachedCredentialsCount.ts okta.com
*
* NOTE: enumeration logic is duplicated from `purgeCachedCredentials.ts`
* (the dry-run "found" branch) — keep in sync if the `security` /
* `cmdkey` enumeration commands change.
*/
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
import { isDarwin, isWin32 } from "./_shared/platform";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "get_cached_credentials_count",
description:
"Reports per-domain cached credential counts in macOS Keychain (login) " +
"or Windows Credential Manager without modifying anything. Read-only " +
"counterpart to purge_cached_credentials.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
tccCategories: [],
schema: {
domains: z
.array(z.string().min(1))
.min(1)
.refine(
(arr) => arr.every((d) => !/[*?]/.test(d)),
{ message: "Wildcard domains are not permitted — supply exact host strings." },
)
.refine(
(arr) => arr.every((d) => /^[a-zA-Z0-9.\-]+$/.test(d)),
{ message: "Domains must contain only letters, digits, dots and dashes." },
)
.describe("Exact domain strings to probe."),
},
} as const;
// -- Types --------------------------------------------------------------------
export interface DomainCredentialCount {
domain: string;
count: number;
sample?: string[];
error?: string;
}
export interface GetCachedCredentialsCountResult {
platform: NodeJS.Platform | "other";
domains: DomainCredentialCount[];
totalCount: number;
}
// -- Helpers ------------------------------------------------------------------
function shellQuote(s: string): string {
return `\'${s.replace(/'/g, `'\\''`)}\'`;
}
async function countDarwin(domain: string): Promise<DomainCredentialCount> {
const sample: string[] = [];
let count = 0;
try {
const { stdout } = await execAsync(
`security find-internet-password -s ${shellQuote(domain)} 2>&1 || true`,
{ maxBuffer: 2 * 1024 * 1024, timeout: 5_000 },
);
const hits = stdout.match(/"acct"<blob>="[^"]*"/g) ?? [];
count += hits.length;
sample.push(...hits.slice(0, 5).map((s) => s.replace(/"acct"<blob>=/, "")));
} catch { /* no match */ }
try {
const { stdout } = await execAsync(
`security find-generic-password -s ${shellQuote(domain)} 2>&1 || true`,
{ maxBuffer: 2 * 1024 * 1024, timeout: 5_000 },
);
const hits = stdout.match(/"acct"<blob>="[^"]*"/g) ?? [];
count += hits.length;
sample.push(...hits.slice(0, 5).map((s) => s.replace(/"acct"<blob>=/, "")));
} catch { /* no match */ }
return { domain, count, sample: sample.slice(0, 10) };
}
async function countWin32(domain: string): Promise<DomainCredentialCount> {
try {
const { stdout } = await execAsync(`cmdkey /list`, {
maxBuffer: 2 * 1024 * 1024, timeout: 5_000,
});
const lines = stdout.split(/\r?\n/);
const targets: string[] = [];
for (const line of lines) {
const m = line.match(/^\s*Target:\s*(.+?)\s*$/);
if (!m) continue;
const raw = m[1].trim();
const target = raw.replace(/^[^:]+:target=/, "");
if (target.toLowerCase().includes(domain.toLowerCase())) {
targets.push(raw);
}
}
return { domain, count: targets.length, sample: targets.slice(0, 10) };
} catch (err) {
return { domain, count: 0, error: (err as Error).message };
}
}
// -- Exported run -------------------------------------------------------------
export async function run({
domains,
}: { domains: string[] }): Promise<GetCachedCredentialsCountResult> {
const platform: "darwin" | "win32" | "other" =
isDarwin() ? "darwin" : isWin32() ? "win32" : "other";
const results: DomainCredentialCount[] = [];
for (const domain of domains) {
if (platform === "darwin") {
results.push(await countDarwin(domain));
} else if (platform === "win32") {
results.push(await countWin32(domain));
} else {
results.push({ domain, count: 0, error: "Unsupported platform" });
}
}
const totalCount = results.reduce((s, r) => s + r.count, 0);
return { platform, domains: results, totalCount };
}
// -- CLI smoke test -----------------------------------------------------------
if (require.main === module) {
const argv = process.argv.slice(2);
const domains = argv.length > 0 ? argv : ["okta.com"];
run({ domains })
.then((r) => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}