/**
* mcp/skills/detectIdentityProvider.ts — detect_identity_provider
*
* Inspects installed agents and domain configuration to infer which
* cloud identity provider the endpoint uses. Returns a single canonical
* idp ("okta" | "entra" | "google" | "unknown") plus any secondary
* candidates and the evidence that led to the decision.
*
* Platform strategy
* -----------------
* darwin Check for /Applications/Okta Verify.app,
* /Library/Application Support/JamfConnect/,
* /Library/Intune/, and Google Credential Provider artefacts.
* win32 Parse `dsregcmd /status` (for AzureAdJoined, WorkplaceJoined),
* registry HKLM\Software\Okta\Okta Verify, and
* HKLM\Software\Google\Credential Provider.
*
* Zero detected → return { primary: "unknown" } — never throw.
* Multiple detected → first entry wins on `primary`; rest go in `secondary`.
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/detectIdentityProvider.ts
*/
import * as fs from "fs" ;
import { z } from "zod" ;
import { isDarwin, isWin32, execAsync, runPS } from "./_shared/platform" ;
import type { Idp } from "./_shared/idp" ;
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "detect_identity_provider" ,
description:
"Detects which cloud identity provider (Okta, Microsoft Entra, or Google " +
"Workspace) the endpoint is joined to. Inspects installed agents and " +
"domain configuration, not user credentials. Returns the primary IDP plus " +
"any secondary detections and the evidence. Use at the start of cloud " +
"password-reset or SSO-repair workflows." ,
riskLevel: "low" ,
destructive: false ,
requiresConsent: false ,
supportsDryRun: false ,
affectedScope: [ "user" ],
auditRequired: false ,
schema: {}, // no params
} as const ;
// -- Types --------------------------------------------------------------------
interface Detection {
idp : Exclude < Idp , "unknown" >;
/** Short reason string — e.g. "Okta Verify installed". */
evidence : string ;
}
export interface IdpDetectionResult {
platform : "darwin" | "win32" | "other" ;
primary : Idp ;
secondary : Array < Exclude < Idp , "unknown" >>;
evidence : string [];
}
// -- darwin implementation ----------------------------------------------------
async function detectDarwin () : Promise < Detection []> {
const detections : Detection [] = [];
// Okta Verify — first-party Okta endpoint companion.
if ( safePathExists ( "/Applications/Okta Verify.app" )) {
detections. push ({ idp: "okta" , evidence: "Okta Verify.app installed" });
}
// Jamf Connect — MDM-driven Okta/Entra integration, commonly signals Okta.
if ( safePathExists ( "/Library/Application Support/JamfConnect" )) {
detections. push ({ idp: "okta" , evidence: "Jamf Connect configuration present" });
}
// Intune — Microsoft-managed endpoints signal Entra.
if ( safePathExists ( "/Library/Intune" )) {
detections. push ({ idp: "entra" , evidence: "Microsoft Intune agent installed" });
}
// Company Portal for Entra.
if ( safePathExists ( "/Applications/Company Portal.app" )) {
detections. push ({ idp: "entra" , evidence: "Microsoft Company Portal installed" });
}
// Google Credential Provider / Workspace agent artefacts.
// Google does not currently ship a Mac password-reset agent, but the
// Workspace Endpoint Verification helper is a reasonable signal.
if ( safePathExists ( "/Applications/Endpoint Verification.app" )) {
detections. push ({ idp: "google" , evidence: "Google Endpoint Verification installed" });
}
// macOS Google Drive indicates Workspace but is a weak signal only —
// do NOT return "google" solely on the basis of Google Drive.
return dedupeByIdp (detections);
}
// -- win32 implementation -----------------------------------------------------
async function detectWin32 () : Promise < Detection []> {
const detections : Detection [] = [];
// dsregcmd /status reports AzureAdJoined / WorkplaceJoined / DomainJoined.
try {
const { stdout } = await execAsync ( "dsregcmd /status" , {
maxBuffer: 2 * 1024 * 1024 , timeout: 10_000 ,
});
if ( / AzureAdJoined \s * : \s * YES / i . test (stdout)) {
detections. push ({ idp: "entra" , evidence: "dsregcmd reports AzureAdJoined: YES" });
} else if ( / WorkplaceJoined \s * : \s * YES / i . test (stdout)) {
detections. push ({ idp: "entra" , evidence: "dsregcmd reports WorkplaceJoined: YES" });
}
} catch {
// dsregcmd may not be on PATH in locked-down environments; skip silently.
}
// Okta Verify registry key.
if ( await winRegistryKeyExists ( "HKLM \\ Software \\ Okta \\ Okta Verify" )) {
detections. push ({ idp: "okta" , evidence: "Okta Verify installed (registry)" });
}
if ( await winRegistryKeyExists ( "HKCU \\ Software \\ Okta \\ Okta Verify" )) {
detections. push ({ idp: "okta" , evidence: "Okta Verify installed (registry, user hive)" });
}
// Google Credential Provider for Windows.
if ( await winRegistryKeyExists ( "HKLM \\ Software \\ Google \\ Credential Provider" )) {
detections. push ({ idp: "google" , evidence: "Google Credential Provider installed" });
}
return dedupeByIdp (detections);
}
// -- Helpers ------------------------------------------------------------------
function safePathExists ( p : string ) : boolean {
try {
return fs. existsSync (p);
} catch {
return false ;
}
}
/**
* Preserve ordering but drop duplicate IDPs so each canonical value
* appears at most once. Evidence for the kept entry is the FIRST one
* encountered — callers can still inspect result.evidence[] for the
* full trail.
*/
function dedupeByIdp ( detections : Detection []) : Detection [] {
const seen = new Set < string >();
const out : Detection [] = [];
for ( const d of detections) {
if (seen. has (d.idp)) continue ;
seen. add (d.idp);
out. push (d);
}
return out;
}
/**
* Query Windows registry by firing `reg query` and checking for a
* non-zero-length output. Returns false on any non-zero exit or error.
*/
async function winRegistryKeyExists ( key : string ) : Promise < boolean > {
if ( ! isWin32 ()) return false ;
const safe = key. replace ( / ["'&|] / g , "" ); // strip shell metacharacters
try {
const { stdout } = await execAsync ( `reg query "${ safe }"` , {
maxBuffer: 1 * 1024 * 1024 , timeout: 5_000 ,
});
return stdout. trim (). length > 0 ;
} catch {
return false ;
}
}
// Exported purely for unit tests that want to fake platform detection.
export const __testing = {
detectDarwin,
detectWin32,
};
// -- Exported run function ----------------------------------------------------
export async function run () : Promise < IdpDetectionResult > {
let detections : Detection [] = [];
let platform : "darwin" | "win32" | "other" = "other" ;
try {
if ( isDarwin ()) {
platform = "darwin" ;
detections = await detectDarwin ();
} else if ( isWin32 ()) {
platform = "win32" ;
detections = await detectWin32 ();
}
} catch {
// Any unexpected error falls through to the "unknown" result.
detections = [];
}
if (detections. length === 0 ) {
return { platform, primary: "unknown" , secondary: [], evidence: [] };
}
// Reference runPS so the import is not flagged unused — it's listed
// alongside execAsync in _shared/platform because win32-specific tools
// in Phase 2 will need it. The detection code here uses execAsync +
// reg query directly.
void runPS;
const [ first , ... rest ] = detections;
return {
platform,
primary: first.idp,
secondary: rest. map (( d ) => d.idp),
evidence: detections. map (( d ) => d.evidence),
};
}
// -- CLI smoke test -----------------------------------------------------------
if ( false ) {
run ()
. then (( r ) => console. log ( JSON . stringify (r, null , 2 )))
. catch (( err : Error ) => { console. error (err.message); process. exit ( 1 ); });
}