check_ntp_status

check_ntp_status

Reports the endpoint's NTP synchronisation status and current offset from a reference time source, in milliseconds. An offset > ~300000 ms (5 minutes) breaks Kerberos, SAML, and TOTP simultaneously — this tool is the first diagnostic step when users report 'all SSO apps are broken.' Read-only; safe to run without consent.

Metadata

Name

check_ntp_status

Updated

2 months ago

Source

GitHub source

Risk

Low

Requires consent

false

Affected scope

user

Code

/**
 * mcp/skills/checkNtpStatus.ts — check_ntp_status
 *
 * Reports the endpoint's NTP configuration + offset from a reference
 * source.  An offset > ~5 min silently breaks Kerberos, SAML, and TOTP
 * simultaneously — this tool is how the identity-auth-repair skill
 * detects that root cause.
 *
 * Platform strategy
 * -----------------
 * darwin  `sntp -d <server>` — unauthenticated NTP query (no admin).
 *         Parses "offset <seconds>" from the debug output.
 * win32   `w32tm /query /status` — reports Source, Phase Offset,
 *         Last Successful Sync Time.  Also surfaces the service state
 *         so we can flag disabled W32Time on workgroup machines.
 *
 * Returns an offset in milliseconds (positive = endpoint is ahead of
 * reference; negative = behind).  Never throws — network / exec
 * failures resolve to { offsetMs: null, status: "error", … }.
 */

import { z } from "zod";

import { execAsync, isDarwin, isWin32 } from "./_shared/platform";

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

export const meta = {
  name: "check_ntp_status",
  description:
    "Reports the endpoint's NTP synchronisation status and current offset " +
    "from a reference time source, in milliseconds. An offset > ~300000 ms " +
    "(5 minutes) breaks Kerberos, SAML, and TOTP simultaneously — this tool " +
    "is the first diagnostic step when users report 'all SSO apps are " +
    "broken.' Read-only; safe to run without consent.",
  riskLevel:       "low",
  destructive:     false,
  requiresConsent: false,
  supportsDryRun:  false,
  affectedScope:   ["user"],
  auditRequired:   false,
  schema: {
    server: z
      .string()
      .optional()
      .describe(
        "Reference NTP server. Defaults to 'time.apple.com' on macOS and " +
        "'time.windows.com' on Windows."
      ),
  },
  outputKeys: [
    "platform",
    "server",
    "offsetMs",
    "absOffsetMs",
    "lastSync",
    "serviceState",
    "status",
    "message",
  ],
} as const;

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

export interface NtpStatusResult {
  platform:    "darwin" | "win32" | "other";
  server:      string;
  /** Offset in milliseconds (positive = endpoint ahead, negative = behind). */
  offsetMs:    number | null;
  /** Absolute value of offsetMs; convenient for threshold checks. */
  absOffsetMs: number | null;
  /** Human-readable last successful sync time (null on darwin fallback or unknown). */
  lastSync:    string | null;
  /** W32Time / timed service state ("running" / "stopped" / "unknown"). */
  serviceState: "running" | "stopped" | "unknown";
  status:      "ok" | "drifted" | "error";
  message:     string;
}

const DRIFT_THRESHOLD_MS = 5 * 60 * 1_000; // 5 min — standard Kerberos tolerance

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

async function checkDarwin(server: string): Promise<NtpStatusResult> {
  try {
    const { stdout } = await execAsync(
      `sntp -d ${shellQuote(server)} 2>&1`,
      { maxBuffer: 1 * 1024 * 1024, timeout: 5_000 },
    );
    const match = stdout.match(/[-+]\d+(?:\.\d+)?\s+\+\/-);
    if (!match) {
      return {
        platform: "darwin", server,
        offsetMs: null, absOffsetMs: null,
        lastSync: null, serviceState: "unknown",
        status: "error",
        message: `sntp returned no parseable offset: ${stdout.slice(0, 200)}`,
      };
    }
    const offsetSeconds = parseFloat(match[0].replace(/\s+\+\/\-$/, "").trim());
    const offsetMs = Math.round(offsetSeconds * 1_000);
    const absMs    = Math.abs(offsetMs);
    const drifted  = absMs > DRIFT_THRESHOLD_MS;
    return {
      platform: "darwin", server,
      offsetMs, absOffsetMs: absMs,
      lastSync:     null,
      serviceState: "unknown",
      status:       drifted ? "drifted" : "ok",
      message: drifted
        ? `Endpoint clock is ${Math.round(absMs / 1000)}s ${offsetMs > 0 ? "ahead of" : "behind"} ${server} — Kerberos/SAML/TOTP will fail.`
        : `Endpoint clock is within ${Math.round(absMs)}ms of ${server}.`,
    };
  } catch (err) {
    return {
      platform: "darwin", server,
      offsetMs: null, absOffsetMs: null,
      lastSync: null, serviceState: "unknown",
      status: "error",
      message: `sntp failed: ${(err as Error).message}`,
    };
  }
}

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

async function checkWin32(server: string): Promise<NtpStatusResult> {
  let serviceState: "running" | "stopped" | "unknown" = "unknown";
  try {
    const { stdout } = await execAsync(`sc query w32time`, {
      maxBuffer: 1 * 1024 * 1024, timeout: 5_000,
    });
    if (/STATE\s*: \s*4\s*RUNNING/i.test(stdout))   serviceState = "running";
    else if (/STATE\s*: \s*1\s*STOPPED/i.test(stdout)) serviceState = "stopped";
  } catch {
  }

try {
    const { stdout } = await execAsync(`w32tm /query /status`, {
      maxBuffer: 1 * 1024 * 1024, timeout: 5_000,
    });
    const offMatch = stdout.match(/Phase Offset\s*:\s*([-+]\d+(?:\.\d+)?)s/i);
    const lastMatch = stdout.match(/Last Successful Sync Time\s*:\s*(.+)/i);
    if (!offMatch) {
      return {
        platform: "win32", server,
        offsetMs: null, absOffsetMs: null,
        lastSync: lastMatch ? lastMatch[1].trim() : null,
        serviceState,
        status: "error",
        message: `w32tm returned no parseable Phase Offset.`,
      };
    }
    const offsetMs = Math.round(parseFloat(offMatch[1]) * 1_000);
    const absMs    = Math.abs(offsetMs);
    const drifted  = absMs > DRIFT_THRESHOLD_MS;
    return {
      platform: "win32", server,
      offsetMs, absOffsetMs: absMs,
      lastSync: lastMatch ? lastMatch[1].trim() : null,
      serviceState,
      status:   drifted ? "drifted" : "ok",
      message: drifted
        ? `Endpoint clock is ${Math.round(absMs / 1000)}s ${offsetMs > 0 ? "ahead of" : "behind"} reference — Kerberos/SAML/TOTP will fail.`
        : `Endpoint clock is within ${absMs}ms of reference (last sync: ${lastMatch ? lastMatch[1].trim() : "unknown"}).`,
    };
  } catch (err) {
    return {
      platform: "win32", server,
      offsetMs: null, absOffsetMs: null,
      lastSync:     null,
      serviceState,
      status: "error",
      message: `w32tm /query /status failed: ${(err as Error).message}`,
    };
  }
}

// -- Helpers ------------------------------------------------------------------

function shellQuote(s: string): string {
  return `'${s.replace(/'/g, `'\\''`)}'`;
}

// Exported for unit tests.
export const __testing = { checkDarwin, checkWin32, DRIFT_THRESHOLD_MS };

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

export async function run({
  server,
}: {
  server?: string;
} = {}): Promise<NtpStatusResult> {
  const platform: "darwin" | "win32" | "other" =
    isDarwin() ? "darwin" : isWin32() ? "win32" : "other";

const defaultServer = platform === "win32" ? "time.windows.com" : "time.apple.com";
  const resolvedServer = server ?? defaultServer;

if (platform === "darwin") return checkDarwin(resolvedServer);
  if (platform === "win32")  return checkWin32(resolvedServer);

return {
    platform: "other", server: resolvedServer,
    offsetMs: null, absOffsetMs: null,
    lastSync: null, serviceState: "unknown",
    status: "error",
    message: "Unsupported platform — cannot check NTP status.",
  };
}

// -- 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); });
}