100% free and open source · every tool, every host, no licence key · read the licence
Secronyx

Tutorials

Tutorial: triage a Windows IIS server

A walkthrough of triaging an IIS server with Secronyx, from site inventory and app pool recycling to worker processes, failed request traces, event logs and platform state, with the exact tool names, arguments and result fields.

An IIS site is returning 502s, or throwing 500s intermittently, or simply falling over every few hours. This walkthrough triages it with Secronyx from the outside in: what sites exist, what state they are in, what the worker processes are doing, what IIS itself has recorded, and what the platform underneath is doing to them. Every tool named here is registered in internal/mcp/tools.go, and every field is a field of the types.IIS* structs in pkg/types/windows.go.

All IIS tools are registered under the windows scope, except where noted. They are Windows-only: on other platforms the collectors return an empty result with an error string rather than failing, which is why almost every IIS result type carries an error field alongside its data.

Step 0: get the agent onto the host

If Secronyx is not already running as a service, install it. The service subcommand wraps the platform service manager, and -- passes the remainder to the installed service:

secronyx.exe service install -- --transport http --listen 0.0.0.0:8443 `
    --tls-cert C:\ProgramData\secronyx\cert.pem `
    --tls-key  C:\ProgramData\secronyx\key.pem `
    --api-keys-file C:\ProgramData\secronyx\keys.json `
    --scopes core,logs,hooks,triage,windows,software,security,report `
    --audit-output C:\ProgramData\secronyx\audit.jsonl
secronyx.exe service start
secronyx.exe service status

The available actions are install, uninstall (or remove), start, stop, restart, status and run, with --name to set the service name (default secronyx). Windows service covers the account it runs under and what that account can and cannot see; that matters here, because IIS configuration reads need the rights to read the applicationHost.config store.

While you are working on the box itself, --query is faster than speaking MCP:

secronyx.exe --query get_iis_sites --json

Step 1: inventory the sites

secronyx.exe --query get_iis_sites --json

get_iis_sites takes no arguments and returns types.IISSitesResult:

{
  "sites": [
    {
      "id": 1,
      "name": "Default Web Site",
      "state": "Started",
      "physical_path": "C:\\inetpub\\wwwroot",
      "bindings": [
        { "protocol": "http", "binding_information": "*:80:",
          "ip_address": "*", "port": 80 }
      ],
      "server_auto_start": true
    },
    {
      "id": 3,
      "name": "api.internal",
      "state": "Started",
      "physical_path": "D:\\sites\\api",
      "bindings": [
        { "protocol": "https", "binding_information": "*:443:api.internal",
          "ip_address": "*", "port": 443, "host_name": "api.internal",
          "certificate_hash": "9F2C...", "certificate_store": "My", "ssl_flags": 1 }
      ],
      "applications": [
        { "path": "/", "application_pool": "api-pool",
          "physical_path": "D:\\sites\\api", "enabled_protocols": "http",
          "preload_enabled": true, "service_auto_start_enabled": false }
      ],
      "log_file_directory": "D:\\logs\\W3SVC3",
      "server_auto_start": true,
      "limits": { "max_bandwidth": 4294967295, "max_connections": 4294967295,
                  "connection_timeout": 120, "max_url_segments": 32 }
    }
  ],
  "count": 2,
  "timestamp": "2026-09-17T09:12:44Z"
}

Note what this already answers. state distinguishes a stopped site from a failing one. applications[].application_pool is the link you will follow for the rest of the investigation — a site's health is mostly its pool's health. log_file_directory tells you where the W3C logs live, which you will need in step 6. bindings[].certificate_hash and ssl_flags matter if the symptom is TLS-shaped; get_iis_ssl_certs and get_iis_bindings enumerate those across every site in one call.

If error is set and sites is empty, either IIS is not installed or the agent's account cannot read the configuration store. Check get_windows_features before you conclude anything.

Step 2: read the runtime counters, not the config

Configuration tells you what should happen. get_iis_site_state tells you what is happening:

{"jsonrpc":"2.0","id":2,"method":"tools/call",
 "params":{"name":"get_iis_site_state","arguments":{"site_name":"api.internal"}}}

The result is types.IISSiteStateResult, whose sites entries carry site_name, site_id, state, current_connections, total_bytes_received, total_bytes_sent, total_connection_attempts and total_requests_served:

{
  "sites": [
    { "site_name": "api.internal", "site_id": 3, "state": "Started",
      "current_connections": 1841,
      "total_bytes_received": 88123441,
      "total_bytes_sent": 9914233812,
      "total_connection_attempts": 2291044,
      "total_requests_served": 2288119 }
  ],
  "timestamp": "2026-09-17T09:13:02Z"
}

current_connections in the thousands against a small worker count is the classic queue-building signature. The gap between total_connection_attempts and total_requests_served is connections that never became served requests — a queue overflow or a reset, not an application error.

site_name is optional on this tool; omit it to get every site.

Step 3: look at the worker processes

{"jsonrpc":"2.0","id":3,"method":"tools/call",
 "params":{"name":"get_iis_worker_processes","arguments":{"app_pool_name":"api-pool"}}}

types.IISWorkerProcessesResult gives one entry per running w3wp.exe:

{
  "processes": [
    { "process_id": 8124, "app_pool_name": "api-pool", "state": "Running",
      "start_time": "2026-09-17T08:58:10Z",
      "cpu_percent": 97.8,
      "memory_working_set_mb": 3921,
      "memory_private_mb": 3610,
      "requests_total": 118422,
      "requests_active": 412 }
  ],
  "count": 1,
  "timestamp": "2026-09-17T09:13:19Z"
}

Three readings, in order of how often they turn out to be the answer:

  • start_time far more recent than the site. The pool has been recycling. Go to step 4.
  • requests_active high and climbing while requests_total barely moves. Requests are entering and not completing: a downstream dependency, a lock, or a thread-pool starvation. Cross-check with get_processes_sampled (scope core) to see whether the CPU is inside w3wp.exe or in a sibling process such as a local SQL Server instance.
  • memory_private_mb near the pool's recycling threshold. The pool is about to restart itself, which produces exactly the intermittent 502 pattern users report. Step 4 gives you the threshold to compare against.

A pool with zero worker processes but a Started site is a pool that failed to start or was shut down by rapid-fail protection.

Step 4: read the pool's recycling, process model and CPU settings

These four tools all take an optional app_pool_name and are the reason most "random restart" tickets resolve:

{"jsonrpc":"2.0","id":4,"method":"tools/call",
 "params":{"name":"get_iis_apppool_recycling","arguments":{"app_pool_name":"api-pool"}}}

Mind the spelling. The registered tool names are get_iis_apppool_recycling, get_iis_apppool_process_model, get_iis_apppool_cpu and get_iis_apppool_failure — one word, no underscore between "app" and "pool". The --query CLI accepts the other spelling for these four (get_iis_app_pool_recycling and so on), plus get_iis_aspnet_machine_key where the tool is get_iis_aspnet_machinekey and get_iis_httpsys_listeners where the tool is get_iis_http_sys_listeners. Use the tools/call spellings when you are speaking MCP and the --query spellings on the command line; tools/list is always authoritative for the former.

Each of the four returns an app_pools array, one entry per pool and each carrying name, plus the usual error and timestamp.

types.IISAppPoolRecyclingResult entries carry disallow_overlapping_rotation, disallow_rotation_on_config_change, log_event_on_recycle and a nested periodic_restart object with memory, private_memory, requests, time and schedule. Note the nesting: the field to read is periodic_restart.private_memory, not a flat periodic_restart_private_memory. If that threshold sits below the 3,610 MB you measured in step 3, you have the whole ticket: the pool is recycling on memory every few minutes, and with disallow_overlapping_rotation set there is a gap during which requests fail.

types.IISAppPoolProcessModelResult entries carry identity_type, user_name, load_user_profile, set_profile_environment, idle_timeout, idle_timeout_action, max_processes, pinging_enabled, ping_interval, ping_response_time, shutdown_time_limit and startup_time_limit. The five timing fields are strings here, carrying the timespan IIS reports ("00:20:00"), not integers. An idle_timeout of "00:20:00" on a low-traffic internal API produces cold starts that users read as outages. A ping_response_time shorter than the application's worst-case GC pause makes IIS kill healthy workers.

types.IISAppPoolCPUResult entries carry limit, action, reset_interval (also a string), smp_affinitized, smp_processor_affinity_mask, smp_processor_affinity_mask2 and numa_node_assignment. An action of KillW3wp with a limit your workload routinely reaches is a self-inflicted outage on a schedule.

get_iis_apppool_failure completes the set: rapid_fail_protection, rapid_fail_protection_interval, rapid_fail_protection_max_crashes, load_balancer_capabilities, orphan_worker_process with orphan_action_exe and orphan_action_params, and auto_shutdown_exe / auto_shutdown_params. Rapid-fail protection is what turns a crash loop into a stopped pool.

For the whole estate at once, get_iis_app_pools (this one is spelled with the underscore, in both the tool and the CLI) returns every pool with name, state, managed_runtime_version, managed_pipeline_mode, enable_32bit_app_on_win64, start_mode, auto_start, queue_length and the nested process_model, recycling and cpu objects. These nested objects are a different, flatter shape from the four tools above: recycling uses periodic_restart_memory (KB), periodic_restart_private_memory, periodic_restart_requests, periodic_restart_time (minutes) and periodic_restart_schedule; process_model reports idle_timeout in minutes and ping_interval in seconds as integers, with identity_type one of LocalSystem, LocalService, NetworkService, ApplicationPoolIdentity or SpecificUser; and cpu reports limit as a percentage multiplied by 1000 (85000 means 85%) with action one of NoAction, KillW3wp, Throttle or ThrottleUnderLoad.

Step 5: pull the failed request traces

IIS's own record of failures is Failed Request Event Buffering. Check the rules first, because an empty result usually means tracing was never enabled:

{"jsonrpc":"2.0","id":5,"method":"tools/call",
 "params":{"name":"get_iis_failed_request_rules","arguments":{"site_name":"api.internal"}}}

Rules come back per site with path, status_codes, time_taken, verbosity, custom_action_exe and failure_definitions (each status_code, sub_status, verbosity). Then read the traces:

{"jsonrpc":"2.0","id":6,"method":"tools/call",
 "params":{"name":"get_iis_failed_requests",
           "arguments":{"site_name":"api.internal","limit":50}}}

limit defaults to 100. types.IISFailedRequestsResult returns requests with site_name, url, verb, status_code, time_taken_ms, fail_time and log_file:

{
  "requests": [
    { "site_name": "api.internal", "url": "/v2/orders/search", "verb": "POST",
      "status_code": 500, "time_taken_ms": 30104,
      "fail_time": "2026-09-17T09:04:51Z",
      "log_file": "D:\\logs\\FailedReqLogFiles\\W3SVC3\\fr000318.xml" },
    { "site_name": "api.internal", "url": "/v2/orders/search", "verb": "POST",
      "status_code": 502, "time_taken_ms": 30002,
      "fail_time": "2026-09-17T09:05:22Z",
      "log_file": "D:\\logs\\FailedReqLogFiles\\W3SVC3\\fr000319.xml" }
  ],
  "count": 2,
  "timestamp": "2026-09-17T09:13:41Z"
}

time_taken_ms clustered at a round number — 30,000 here — is a timeout, and one URL dominating tells you which handler to look at. log_file gives the operator the exact XML trace to open by hand; Secronyx summarises, it does not fetch the file contents.

Keep limit modest. Results are capped by --max-result-bytes (4 MiB by default); over the cap you get a normal result with "isError": true whose text reads Error: result too large: N bytes exceeds the M byte cap; narrow the query (limit, lines, filters).

If tracing is off, get_iis_logging tells you what W3C logging is configured instead: server_level and per-site config objects with log_format, directory, enabled, local_time_rollover, period, truncate_size and log_ext_file_flags. log_ext_file_flags is the field to check when someone says "the logs do not have the field I need".

Step 6: read the Windows logs

{"jsonrpc":"2.0","id":7,"method":"tools/call",
 "params":{"name":"get_event_log","arguments":{"log_name":"Application","lines":200}}}

get_event_log is scope logs, not windows. It takes lines (default 50) and log_name (default System); the handler maps log_name onto types.EventLogQuery.Channel, so the values are channel names — Application, System, Security. The result is types.EventLogResult: a LogResult with entries (timestamp, source, level, message, pid, fields), source, count, truncated, plus channel echoing back which log was read.

Application is where the WAS and IIS-W3SVC-WP events land, which is how you confirm a recycle you inferred in step 4. get_windows_event_log_list (scope windows) gives the inventory and configuration of the key logs, which is how you find out that Application is capped at 20 MB and has been overwriting itself all afternoon.

Step 7: check the platform underneath

A pool that will not stay up is sometimes not the pool's fault.

secronyx.exe --query get_windows_pending_reboot --json
secronyx.exe --query get_win_boot_performance --json
secronyx.exe --query get_windows_hotfixes --json

get_windows_pending_reboot returns component_based_servicing, windows_update, pending_file_rename and the roll-up reboot_pending. A host with pending_file_rename true has had files replaced under a running process, which produces exactly the "it worked yesterday" class of failure.

get_win_boot_performance returns last_boot_time_ms, last_shutdown_time_ms, boot_type (FullBoot, HybridBoot or ResumeFromHibernation), graceful_shutdowns, unexpected_shutdowns, blue_screens and boot_phases. A non-zero unexpected_shutdowns or blue_screens count moves the investigation off IIS entirely.

get_windows_hotfixes is scope software; get_windows_update_health and get_windows_defender_status are scope security. If your agent was started with --scopes core,logs,hooks,triage,windows those tools are never registered, so they are absent from tools/list and a tools/call for one comes back as JSON-RPC error -32602 with the message Tool not found. The separate -32003 Forbidden error is for a registered tool that the authenticated caller's own granted scopes do not cover.

Round it out with the generic tools from Tutorial: diagnose a slow Linux host, which are cross-platform: get_cpu_info, get_memory_info, get_disk_info, get_listening_ports (scope hooks, and the fastest way to see whether http.sys is even holding :443) and get_processes_sampled.

Step 8: one call for the written record

When you need an artefact to attach to the ticket rather than a sequence of readings:

{"jsonrpc":"2.0","id":8,"method":"tools/call",
 "params":{"name":"generate_iis_report",
           "arguments":{"sections":["sites","app_pools","bindings","ssl_certs","auth_config"],
                        "timeout_seconds":60}}}

generate_iis_report is scope report. It collects in parallel and returns JSON suitable for binding to an HTML template. The section names it accepts are sites, app_pools, bindings, virtual_dirs, handlers, modules, ssl_certs and auth_config; omit sections for all of them. timeout_seconds defaults to 30 and bounds the whole parallel collection, not each collector. generate_system_report is the cross-platform equivalent, with sections os, hardware, uptime, cpu, memory, gpu, processes, disks, network, listening_ports, dns, routes, arp, startup_items, programs and runtimes.

A worked conclusion

Reading the steps back in order gives a finding with no inference in it:

api.internal (site 3) is Started with 1,841 current connections and a 2,925-request gap between connection attempts and requests served. Its single w3wp.exe (PID 8124) started at 08:58, fifteen minutes before the reading, with 3,610 MB private bytes and 412 active requests. api-pool has a periodic_restart.private_memory threshold below that figure, with disallow_overlapping_rotation set, so each recycle drops in-flight requests. Failed request traces show /v2/orders/search failing at 30,002-30,104 ms, a cluster tight enough to be a fixed timeout rather than variable application latency. Platform: no pending reboot, no unexpected shutdowns. The recycle threshold and the overlap setting are the fault; the 502s are its symptom.

Where to go next

Built 2026-09-19. Source: levantar-ai/secronyx. Found a mistake? Tell us.