Skip to content

Website checks ​

The web plugin fetches a URL on a schedule and checks the status code and the page body. Turn it on to catch a site that is down, a page that carries injected code, or a page whose expected content has vanished. It can also check the origin server directly, behind a CDN, so a CDN serving cached pages can't hide a dead backend.

Each endpoint becomes its own service on the dashboard, with the unique id web-<name> (or web-<url> when the endpoint has no name).

Enable it ​

Add web to the plugins array. Endpoints can come from two places, and the plugin runs both at once.

Endpoints in the config file ​

json
{
  "plugins": ["web"],
  "web": {
    "refreshInterval": 60000,
    "endpoints": [
      {
        "name": "example.com",
        "url": "https://www.example.com/",
        "expectedStrings": ["Example Domain"],
        "unexpectedStrings": ["id=\"injected-script\"", "malware.example.net"],
        "timeout": 15000
      },
      {
        "name": "example.com xmlrpc (should be blocked)",
        "url": "https://www.example.com/xmlrpc.php",
        "expectedStatusCode": 403
      },
      {
        "name": "shop.example.com",
        "url": "https://shop.example.com/",
        "origin": {
          "url": "https://origin.example.com/",
          "host": "shop.example.com",
          "query": { "cf": "1" },
          "insecure": true
        }
      }
    ]
  }
}

Instead of endpoints, a single endpoint can sit at the top level of the web section (name, url, expectedStrings, unexpectedStrings, expectedStatusCode, timeout, headers, origin). The shorthand does not carry originOnly.

Endpoints managed in the dashboard ​

Monitors can also live in the web_monitors table and be edited under Settings, Web Monitors. Each monitor belongs to one system. The client running that system pulls its enabled monitors from GET /api/monitors/for-client, authenticated by the x-system-key header.

The plugin only does this when the web section names both the endpoint and the key. The client does not copy the connector's key in for you, so repeat it here.

json
{
  "plugins": ["web"],
  "web": {
    "refreshInterval": 60000,
    "configUrl": "https://daemonitor.example.com/api/monitors/for-client",
    "systemKey": "YOUR_SYSTEM_KEY"
  }
}

The plugin fetches the list at the start of every pass. When both sources define an endpoint with the same name and url, the dashboard copy wins. Endpoints that only exist in the config file keep running alongside. If a fetch fails, the plugin keeps running the last list it fetched. If the very first fetch fails, only the local endpoints run.

The settings form can set the name, URL, system, expected status code and the origin check (URL, Host override, ?cf=1, ignore TLS errors). It has no fields for expectedStrings, unexpectedStrings, timeout or originOnly. The table's config column holds them and for-client passes them through, but saving the form rebuilds config from the form fields alone, so editing a monitor in the UI drops them.

Options ​

Section options:

OptionDefaultWhat it does
refreshInterval60000Milliseconds between passes.
endpointsnoneArray of endpoints, below.
configUrlnoneURL of /api/monitors/for-client. Needs systemKey.
systemKeynoneThe system key sent as x-system-key to configUrl.

Endpoint options:

OptionDefaultWhat it does
namethe URLDisplay name. Also used in the unique id and in alert titles.
urlrequiredThe public URL. Redirects are followed.
expectedStatusCodeany 200 to 399Exact status the response must have.
expectedStrings[]Substrings that must appear in the body. A missing one is treated as possible defacement.
unexpectedStrings[]Substrings that must not appear. A match is treated as possible compromise.
timeout30000Milliseconds before the public request is aborted. Covers the body as well as the headers.
headersnoneHeaders sent on the public request, for example a key a WAF skip rule matches. Config file only, for-client does not pass it.
originnoneA second, direct request to the origin server, below.
originOnlyfalseSkip the public request and report the origin check as the endpoint's health. Use it when a bot challenge blocks the checker on the public URL. Needs origin.url.

Origin options:

OptionDefaultWhat it does
urlrequiredAn address that reaches the origin directly, by hostname or IP.
hostnoneHost header to send, so the origin's vhost routes the request like public traffic.
headersnoneExtra headers for the origin request.
querynoneQuery parameters added to the origin URL.
insecurefalseSkip TLS certificate checks on the origin request.
timeoutendpoint timeout, else 10000Milliseconds before the origin request is abandoned.
expectedStrings, unexpectedStrings, expectedStatusCodethe endpoint's valuesOverride how the origin response is judged.

The web_monitors table also has an interval_ms column, 120000 by default. The plugin does not read it. refreshInterval sets the pace for every endpoint.

What it reports ​

One payload per endpoint:

  • name, url. After redirects, url is the final URL.
  • status, the HTTP status, or 0 when the request failed.
  • duration in milliseconds.
  • ok, true when the status passed and the content checks passed. This is the public check only.
  • redirected.
  • hasAllExpectedStrings, hasAnyUnexpectedStrings, missingExpected, matchedUnexpected. The lists name the exact strings, so an alert can say which IOC matched.
  • error, on a network failure or timeout.
  • origin, when configured, with its own url, host, status, duration, ok, error and the same content fields.
  • originOnly: true on origin-only endpoints. Their top-level fields are the origin result.

When it alerts ​

Web checks use the findings model. Each problem is its own alert, keyed by a stable fingerprint, and it alerts on the first report that shows it. When a later report no longer contains that finding, the server resolves it. The same finding comes back as a new alert only after the earlier one has resolved, and a repeat within 30 minutes of the resolve is recorded without paging.

The server checks these rules in order and stops at the first match, so one report raises at most one finding:

FindingSeverityTrigger
UnreachableerrorThe payload has error, a connection failure or timeout.
Possible compromiseerrorhasAnyUnexpectedStrings is true on the public check. The key includes the matched strings, so a new IOC alerts again even while an older one is open.
Possible defacementerrorhasAllExpectedStrings is false. The key includes the missing strings.
Check failederrorok is false for another reason, usually the wrong status code.
Possible compromise at originerrororigin.hasAnyUnexpectedStrings is true while the public check passed.
Origin downwarningorigin.ok is false while the public check passed. Visitors are still served, probably from the CDN cache.

One more rule sits outside the findings model. When nothing above matched and the response took more than 5000 ms, the service goes to warning with "slow". That alert goes through the normal path. It waits for a second bad report before notifying, and it resolves when the service reports online again.

On the dashboard ​

The card is labelled "Web check", or "Web check · origin" when an origin check is configured. Its ping slot shows duration.

  • STATUS shows the HTTP status. It turns red only when ok is false and the status is outside 200 to 399. A 200 page that fails a content check keeps a green STATUS, and the CHECKS list carries the failure.
  • LATENCY shows duration and is never colored.
  • ORIGIN and ORIGIN MS appear with an origin check. ORIGIN shows DOWN on an error and turns red whenever origin.ok is false.
  • The CHECKS list has an HTTP row and an ORIGIN row. A CONTENT row appears when expected strings are missing, and an UNEXPECTED row when forbidden strings matched. Both are red. No content row appears on a passing check, because the payload can't tell "all strings found" from "no strings configured".

The card's overall color comes from the status the server stored. A web check with no report for 300 seconds shows OFFLINE.

Requirements ​

  • Network access from the client to each URL, and to each origin URL.
  • For dashboard-managed monitors, configUrl and systemKey in the web section, and the monitor assigned to this client's system.

Gotchas ​

  • Matching is plain substring matching on the raw body. No regex, no case folding.
  • A pass runs its endpoints one after another, and only one pass runs at a time. With many endpoints near their 30 second timeout, a pass can outlast refreshInterval, and the overlapping tick is skipped.
  • When an origin is dialed by hostname, TLS validates against that hostname, not the host override. When dialed by IP, the certificate needs an IP SAN, or set insecure.
  • An endpoint without a name uses its URL as the alert label. After a redirect the reported url changes, and so does the label.
  • client/README.md lists endpoint options such as method, body, interval and expectedResponseTime. The plugin reads none of them.