check_agent_logs
check_agent_logs
Retrieves recent log entries from a security agent's log file to identify errors, connectivity issues, or policy problems. Use when an agent process is running but behaving incorrectly.
Metadata
Name
check_agent_logs
Updated
2 months ago
Source
Risk
Low
Requires consent
false
Affected scope
user
Code
/**
* mcp/skills/checkAgentLogs.ts — check_agent_logs skill
*
* Retrieves recent log entries from a security agent's log file to identify
* errors, connectivity issues, or policy problems. Use when an agent process
* is running but behaving incorrectly.
*
* Platform strategy
* -----------------
* darwin `tail -n {lines} {logPath}` with optional grep for error keywords
* win32 PowerShell Get-EventLog for the relevant event source
*
* Smoke test
* npx tsx -r dotenv/config mcp/skills/checkAgentLogs.ts
*/
import * as os from "os";
import * as nodePath from "path";
import { exec } from "child_process";
import { promisify } from "util";
import { z } from "zod";
const execAsync = promisify(exec);
// -- Meta ---------------------------------------------------------------------
export const meta = {
name: "check_agent_logs",
description:
"Retrieves recent log entries from a security agent's log file to identify " +
"errors, connectivity issues, or policy problems. Use when an agent process " +
"is running but behaving incorrectly.",
riskLevel: "low",
destructive: false,
requiresConsent: false,
supportsDryRun: false,
affectedScope: ["user"],
auditRequired: false,
tccCategories: ["FullDiskAccess"],
schema: {
agent: z
.enum(["crowdstrike", "sentinelone", "jamf", "carbonblack", "cylance", "defender"])
.describe("Security agent to get logs for"),
lines: z
.number()
.optional()
.describe("Number of recent log lines. Default: 50"),
errorOnly: z
.boolean()
.optional()
.describe("Return only lines containing ERROR, WARN, or FAIL. Default: false"),
},
} as const;
// -- Types --------------------------------------------------------------------
interface AgentLogsResult {
agent: string;
logPath: string | null;
accessible: boolean;
entries: string[];
errorCount: number;
warningCount: number;
message: 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: 20 * 1024 * 1024 },
);
return stdout.trim();
}
// -- darwin: resolve log path -------------------------------------------------
async function resolveDarwinLogPath(agent: string): Promise<string | null> {
switch (agent) {
case "crowdstrike":
return "/var/log/crowdstrike/falconctl.log";
case "sentinelone": {
// Find the newest file in the directory
try {
const { stdout } = await execAsync(
"ls -t /var/log/sentinelone/ 2>/dev/null | head -1",
{ maxBuffer: 1024 * 1024, shell: "/bin/bash" },
);
const fname = stdout.trim();
return fname ? nodePath.join("/var/log/sentinelone", fname) : "/var/log/sentinelone";
} catch {
return "/var/log/sentinelone";
}
}
case "jamf":
return "/private/var/log/jamf.log";
case "defender": {
try {
const { stdout } = await execAsync(
"ls -t /Library/Logs/Microsoft/mdatp/ 2>/dev/null | head -1",
{ maxBuffer: 1024 * 1024, shell: "/bin/bash" },
);
const fname = stdout.trim();
return fname
? nodePath.join("/Library/Logs/Microsoft/mdatp", fname)
: "/Library/Logs/Microsoft/mdatp";
} catch {
return "/Library/Logs/Microsoft/mdatp";
}
}
case "carbonblack":
return "/var/log/CbOsxSensor.log";
case "cylance":
return "/var/log/cylance/cyagent.log";
default:
return null;
}
}
// -- darwin implementation ----------------------------------------------------
async function checkAgentLogsDarwin(
agent: string,
lines: number,
errorOnly: boolean,
): Promise<AgentLogsResult> {
const logPath = await resolveDarwinLogPath(agent);
if (!logPath) {
return {
agent, logPath: null, accessible: false, entries: [],
errorCount: 0, warningCount: 0, message: `No known log path for agent: ${agent}`,
};
}
let rawOutput = "";
let accessible = true;
try {
const safePath = logPath.replace(/'/g, `''`);
if (errorOnly) {
const { stdout } = await execAsync(
`tail -n ${lines} '${safePath}' 2>/dev/null | grep -iE 'ERROR|WARN|FAIL'`,
{ maxBuffer: 5 * 1024 * 1024, shell: "/bin/bash" },
);
rawOutput = stdout;
} else {
const { stdout } = await execAsync(
`tail -n ${lines} '${safePath}' 2>/dev/null`,
{ maxBuffer: 5 * 1024 * 1024, shell: "/bin/bash" },
);
rawOutput = stdout;
}
} catch (err) {
const msg = (err as Error).message;
if (msg.includes("Permission denied") || msg.includes("No such file")) {
accessible = false;
}
rawOutput = (err as { stdout?: string }).stdout ?? "";
}
const entries = rawOutput.trim().split("\n").filter(Boolean);
const errorCount = entries.filter((l) => /error/i.test(l)).length;
const warningCount = entries.filter((l) => /warn/i.test(l)).length;
return {
agent,
logPath,
accessible,
entries,
errorCount,
warningCount,
message: accessible
? `Retrieved ${entries.length} log lines from ${logPath}`
: `Log file not accessible: ${logPath} (may require elevated privileges)`,
};
}
// -- win32 implementation -----------------------------------------------------
const WIN32_EVENT_SOURCES: Record<string, string> = {
crowdstrike: "CSFalconService",
sentinelone: "SentinelAgent",
defender: "Microsoft Antimalware",
carbonblack: "CbDefense",
cylance: "Cylance",
jamf: "Jamf",
};
async function checkAgentLogsWin32(
agent: string,
lines: number,
_errorOnly: boolean,
): Promise<AgentLogsResult> {
const source = WIN32_EVENT_SOURCES[agent] ?? agent;
const ps = `
$ErrorActionPreference = 'SilentlyContinue'
$entries = Get-EventLog -LogName Application -Source '${source}' -Newest ${lines} -EntryType Error,Warning -ErrorAction SilentlyContinue |
Select-Object TimeGenerated,EntryType,Message
if ($entries) { @($entries) | ConvertTo-Json -Depth 2 -Compress } else { '[]' }`.trim();
let entries: string[] = [];
let accessible = true;
try {
const raw = await runPS(ps);
const parsed = JSON.parse(raw ?? "[]") as Array<{
TimeGenerated: string;
EntryType: string;
Message: string;
}>;
entries = parsed.map(
(e) => `[${e.TimeGenerated}] ${e.EntryType}: ${e.Message?.slice(0, 200) ?? ""}`,
);
} catch {
accessible = false;
}
const errorCount = entries.filter((l) => /error/i.test(l)).length;
const warningCount = entries.filter((l) => /warn/i.test(l)).length;
return {
agent,
logPath: `Windows Event Log / Application / ${source}`,
accessible,
entries,
errorCount,
warningCount,
message: accessible
? `Retrieved ${entries.length} event log entries for ${source}`
: `Could not read Windows Event Log for source: ${source}`,
};
}
// -- Exported run function ----------------------------------------------------
export async function run({
agent,
lines = 50,
errorOnly = false,
}: {
agent: "crowdstrike" | "sentinelone" | "jamf" | "carbonblack" | "cylance" | "defender";
lines?: number;
errorOnly?: boolean;
}) {
const platform = os.platform();
return platform === "win32"
? checkAgentLogsWin32(agent, lines, errorOnly)
: checkAgentLogsDarwin(agent, lines, errorOnly);
}
// -- CLI smoke test -----------------------------------------------------------
if (false) {
run({ agent: "jamf" })
.then(r => console.log(JSON.stringify(r, null, 2)))
.catch((err: Error) => { console.error(err.message); process.exit(1); });
}