ZenoScan
Knowledge Base

Guides & Troubleshooting

The most common issues seen while verifying the server-side connector and exactly how to fix each one, plus how to pull your scan data out of ZenoScan with the REST API.

1. File & folder permissions

The connector is a single PHP file that runs as your web server user and writes a small state file (.zenoscan_state.json) next to itself to keep the file-integrity baseline. If the file or its directory has the wrong permissions or owner, verification may pass but scans never progress, or the web server refuses to execute the file.

Symptoms

  • Verify succeeds but the scan stays at 0% or never baselines.
  • Permission denied or failed to open stream in the site's PHP error log.
  • HTTP 403 returned by the web server (not a WAF) when opening the connector URL.

Correct permissions

PathModeOwner
zenoscan_<id>.php644web/site user
Its directory (web root)755web/site user
.zenoscan_state.json (auto-created)644, writableweb/site user

Fix

# from the web root where the connector was uploaded chown $(stat -c '%U' index.php) zenoscan_*.php chmod 644 zenoscan_*.php # the directory must be writable by the web user so the state file can be created chmod 755 .

On cPanel/Plesk the site user usually owns the docroot already, so match the connector's owner to whatever owns index.php or wp-config.php in the same folder.

2. Connector 404 / not reachable

ZenoScan verifies by requesting the connector over HTTPS. A 404 (or "not reachable") almost always means the file is not where the public URL points, the filename does not match, or a redirect is rewriting the request.

Symptoms

  • Verify shows Could not verify: HTTP 404.
  • Opening the connector URL in a browser shows the site's 404 page.

Checklist

  • Right folder: upload the file to the web root, the same directory as index.php / wp-config.php, not a subfolder like /wp-content.
  • Exact filename: keep the full name ZenoScan generated (e.g. zenoscan_5701ca51f9c4a62009ec4185.php). Renaming breaks the URL.
  • No redirect: if the site forces http → https or non-www → www, use the final URL. A 301/302 on the connector path will read as unreachable.
  • Case sensitivity: Linux paths are case-sensitive, so the URL must match the filename exactly.

Confirm it is live

Open the connector URL directly. A correctly installed connector responds with a plain ZenoScan connector active page (no logic or secrets are exposed). If you see that page, press Verify connector again.

curl -sI https://yourdomain.com/zenoscan_<id>.php # expect: HTTP/2 200 (a 301/302/404 here is the problem to fix)

3. 403 / WAF / Varnish block

A firewall or cache in front of the site can block either the connector request (403) or the large scan response (503). This is common on Cloudflare, ModSecurity, CSF/LFD and Varnish-fronted Magento (Breeze).

Symptoms

  • HTTP 403 on verify even though the file exists and is 200 when fetched from the server itself.
  • HTTP 503 or Error 503 Backend fetch failed only on the file scan (Magento / Varnish) with workspace overflow in varnishlog.

Fix: WAF (Cloudflare / ModSecurity / CSF)

  • Add an allow / skip rule for the connector path /zenoscan_*.php so the WAF does not challenge or block it.
  • ModSecurity: whitelist the URI (or the triggering rule IDs) for that path.
  • CSF/LFD: if the scanner IP got temporarily banned, unblock it and add it to csf.allow.

Fix: Varnish (Magento / Breeze)

The large scan response can overflow Varnish's backend workspace. Pipe the connector URL past Varnish so the response is streamed as a raw tunnel and never buffered. In your vcl_recv:

sub vcl_recv { if (req.url ~ "^/zenoscan_[0-9a-f]+\.php") { return (pipe); } }

Use pipe, not pass. pass still fetches and processes the response and will overflow the workspace again. Reload with varnishreload after editing the VCL.

4. 500 / memory / big-site timeout

On large sites (tens of thousands of files, or a big Magento install) the connector can hit a PHP memory limit or execution timeout while building a scan batch, returning HTTP 500 with an empty body.

Symptoms

  • HTTP 500 with 0 bytes on the file scan (verify, which is a small response, still works).
  • Allowed memory size … exhausted or Maximum execution time … exceeded in the PHP error log.

First scan on a big site is expected to be slow

The connector scans in bounded batches (about 30 MB of changed content per pass) and ZenoScan auto-continues to the next batch. The first full baseline of a large site can take several minutes and resume on its own. A scan that is simply progressing slowly is normal, not an error.

Fix

  • Ensure PHP has room. The connector already sets memory_limit=1024M and set_time_limit(0), but a hard cap in php.ini or .user.ini can override it, so set memory_limit to at least 256M.
  • Raise PHP-FPM's request_terminate_timeout if requests are being killed mid-batch.
  • Check max_execution_time isn't forced low by the host.
; php.ini (or per-site .user.ini) memory_limit = 256M max_execution_time = 300

If 500s persist after this, the PHP error log for the site is the fastest way to see the exact limit being hit. Quote it when you open a ticket and we'll tune it with you.

5. REST API: fetching your domains

Everything on your dashboard is available over a JSON API, so you can pull your domains and findings into a spreadsheet, a ticketing system, or an automation such as n8n or Zapier without logging in.

Get a token

Open Settings in the dashboard and create an API token. The token is shown once, at creation time, and only a hash of it is stored, so save it somewhere safe. If you lose it, delete it and create another.

Send it as a Bearer token on every request:

export ZS=zs_your_token_here export API=https://zenoscan.zenocloud.io curl -s -H "Authorization: Bearer $ZS" $API/api/v1/summary

A token only ever sees your own domains

Every endpoint is scoped to the account the token belongs to. There is no global view: another customer's domains never appear in your results, and requesting a site ID that is not yours returns 404. If your account is part of a team, the token sees that team's domains and nothing else.

Endpoints

EndpointWhat it returns
GET /api/v1/summaryEvery domain with its malware and vulnerability counts, connector state and last scan, plus account totals. Start here.
GET /api/v1/findingsFindings across all your domains in one call. Filterable and paginated.
GET /api/v1/sitesYour domain list with counts and connector status.
POST /api/v1/sitesAdd a domain. Body: {"domain":"example.com"}
POST /api/v1/sites/:id/scanStart a scan now.
GET /api/v1/sites/:id/findingsFindings for one domain, paginated.

List every domain

curl -s -H "Authorization: Bearer $ZS" $API/api/v1/summary

Each entry carries the numbers you would otherwise have to open the dashboard to read:

{ "totals": { "domains": 12, "with_issues": 3, "not_scanned": 1 }, "domains": [ { "site_id": 41, "domain": "example.com", "malware_files": 27, "vulnerable_components": 5, "connector": "verified", "last_scan": "2026-08-04T03:12:00.000Z", "scanned": true, "has_issues": true } ] }

has_issues and scanned are worked out for you, so every integration agrees on what counts as a problem. Note that scanned:false means no verified connector is installed and no file on that server was read. A domain in that state is not a clean result, it is an unanswered question.

Only the domains that need attention

curl -s -H "Authorization: Bearer $ZS" $API/api/v1/summary \ | jq -r '.domains[] | select(.has_issues) | "\(.domain)\t\(.malware_files) files\t\(.vulnerable_components) components"'

Findings

One call covers every domain. Narrow it with query parameters:

ParameterValues
domainRestrict to one domain.
typemalware or vulnerability. Omit for both.
severitycritical,high,medium,low. Comma-separated.
statusDefaults to active. Use all to include items you have whitelisted or resolved.
sinceISO date. Only findings first seen on or after it.
limit / offsetPage size (default 100, maximum 1000) and where to start.
# every high-severity finding on one domain curl -s -H "Authorization: Bearer $ZS" \ "$API/api/v1/findings?domain=example.com&severity=high&limit=1000" # only vulnerable components, across all domains curl -s -H "Authorization: Bearer $ZS" \ "$API/api/v1/findings?type=vulnerability" # anything new since the start of the month curl -s -H "Authorization: Bearer $ZS" \ "$API/api/v1/findings?since=2026-08-01"

Paging through a large site

A compromised site can hold tens of thousands of findings, so results are always paged. Keep increasing offset while has_more is true.

curl -s -H "Authorization: Bearer $ZS" "$API/api/v1/findings?limit=1000&offset=0" curl -s -H "Authorization: Bearer $ZS" "$API/api/v1/findings?limit=1000&offset=1000" # response: { "total": 12601, "returned": 1000, "has_more": true, "findings": [ ... ] }

Using it from n8n or Zapier

Add an HTTP Request node, method GET, URL https://zenoscan.zenocloud.io/api/v1/summary, and one header: Authorization set to Bearer <your token>.

The efficient pattern is to call /summary once, keep only the entries where has_issues is true, and then call /findings?domain=... for just those. On a typical fleet only a handful of domains need action in any given month, so this is a few requests rather than one per domain.

In n8n, if the HTTP Request node sits downstream of a node that emits several items, set Execute Once on it. Without that, n8n fires one request per input item.

Responses you should handle

CodeMeaning
200Success.
401Missing, malformed or revoked token.
403Token belongs to a read-only team member and the call writes.
404That site ID does not belong to your account.

Treat a token like a password. It can read every domain and finding on your account. Delete it from Settings the moment it is no longer needed, or if it has been pasted anywhere it should not have been.

Still stuck? Open a ticket with the connector URL and the relevant lines from your PHP error log, and we'll help you get verified.