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