From ab731d398769267aef8cbc95661e29a03df76381 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 05:04:11 +0000 Subject: [PATCH 1/4] Make ZeitHawk's CORS proxy endpoint configurable, add a Worker option MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a "Proxy endpoint" field next to the CORS proxy toggle so it can be pointed at a private proxy instead of the default public corsproxy.io. Includes a ready-to-deploy Cloudflare Worker (tools/zeithawk/cors-proxy-worker/) that does the job under your own account, plus copy-paste deployment steps. The existing Apps Script backend can't fill this role: Apps Script web apps have no way to set Access-Control-Allow-Origin on their own response at all (that's why Code.gs uses JSONP for its one read path instead of a proxy) — a Cloudflare Worker can set CORS headers freely, so it's the viable private-proxy option. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SkupxCaLk9Nnq24YhigdAn --- tools/zeithawk/README.md | 37 +++++--- tools/zeithawk/cors-proxy-worker/README.md | 57 ++++++++++++ tools/zeithawk/cors-proxy-worker/worker.js | 101 +++++++++++++++++++++ tools/zeithawk/index.html | 34 +++++-- 4 files changed, 209 insertions(+), 20 deletions(-) create mode 100644 tools/zeithawk/cors-proxy-worker/README.md create mode 100644 tools/zeithawk/cors-proxy-worker/worker.js diff --git a/tools/zeithawk/README.md b/tools/zeithawk/README.md index 96f5e09..bc17e58 100644 --- a/tools/zeithawk/README.md +++ b/tools/zeithawk/README.md @@ -52,26 +52,26 @@ zero-backend tool. For anything that needs true request interception or raw socket control, use a real intercepting proxy (Burp Suite, OWASP ZAP, mitmproxy). -### CORS proxy toggle (on by default) +### CORS proxy toggle (on by default, configurable endpoint) A switch in the header routes Repeater, Intruder, Recon's DNS/RDAP -lookups, and Vuln Scan through the public proxy -[corsproxy.io](https://corsproxy.io), which fetches the target +lookups, and Vuln Scan through a proxy, which fetches the target server-side and returns the response with permissive CORS headers — letting ZeitHawk reach targets that don't set their own. It's **on by default** since that's the point of the toggle, but it comes with real tradeoffs: -- **It's a third party.** The target URL, and anything you send it - (headers, body, Intruder payloads), passes through corsproxy.io's - servers, which you don't control and can't audit. Turn it off for - anything sensitive, internal, or already CORS-permissive. +- **Whatever endpoint is set is a third party you don't fully control** + (unless you deployed it yourself — see below). The target URL, and + anything you send it (headers, body, Intruder payloads), passes through + it. Turn the toggle off for anything sensitive, internal, or already + CORS-permissive. - **It doesn't change Vuln Scan's honesty guarantee.** Header checks still only ever report "Present" when actually read, never a false "Missing" — - because whether corsproxy.io itself exposes the target's original - headers via `Access-Control-Expose-Headers` isn't something ZeitHawk can - verify, on or off. -- **Method/header/body forwarding depends on corsproxy.io's own behavior**, + because whether the configured proxy itself exposes the target's + original headers via `Access-Control-Expose-Headers` isn't something + ZeitHawk can verify, on or off. +- **Method/header/body forwarding depends on the proxy's own behavior**, not ZeitHawk's — if a request behaves differently proxied vs. direct, toggle it off and compare. - Recon's port-reachability check is deliberately **not** proxied: it uses @@ -79,8 +79,19 @@ tradeoffs: through a proxy would only change the network vantage point, not fix anything. -State persists per-browser via `localStorage`; nothing about the toggle -itself is sent anywhere. +**Default endpoint** is the public [corsproxy.io](https://corsproxy.io). +A **Proxy endpoint** field beneath the toggle lets you point it at a +private proxy instead — see `tools/zeithawk/cors-proxy-worker/` for a +ready-to-deploy Cloudflare Worker that does the same job under your own +account rather than a public third party. (A Google Apps Script web app +*can't* fill this role: it has no way to set +`Access-Control-Allow-Origin` on its own response at all, so a plain +`fetch()` could never read what it returned — see +`tools/apps-script-backend/Code.gs` for where that limitation is +documented, from when that backend was built for a different purpose.) + +Both the on/off state and the endpoint persist per-browser via +`localStorage`; nothing about the toggle itself is sent anywhere. **Authorized testing only.** Only point this at systems you own or have explicit permission to test. diff --git a/tools/zeithawk/cors-proxy-worker/README.md b/tools/zeithawk/cors-proxy-worker/README.md new file mode 100644 index 0000000..824d592 --- /dev/null +++ b/tools/zeithawk/cors-proxy-worker/README.md @@ -0,0 +1,57 @@ +# ZeitHawk CORS proxy (Cloudflare Worker) + +A private alternative to routing ZeitHawk's requests through the public +`corsproxy.io`. Same idea (fetch the target server-side, hand back the +response with permissive CORS headers), but it's your own deployment +instead of a third party you don't control — and unlike a Google Apps +Script web app, a Cloudflare Worker can actually set +`Access-Control-Allow-Origin` on its response, which Apps Script cannot +do at all (see `tools/apps-script-backend/Code.gs` for why that backend +uses JSONP instead of a proxy). + +## Deploy it (no CLI needed, ~2 minutes) + +1. Sign up for a free Cloudflare account at + [dash.cloudflare.com/sign-up](https://dash.cloudflare.com/sign-up) if + you don't have one — the free tier (100,000 requests/day) is plenty + for this. +2. In the dashboard, go to **Workers & Pages** → **Create** → **Create + Worker**. +3. Give it a name (e.g. `zeithawk-proxy`) → **Deploy** (this creates a + placeholder — you'll replace its code next). +4. Click **Edit code** (or "Quick edit") to open the online editor. +5. Delete the placeholder code and paste in the contents of + [`worker.js`](./worker.js) from this folder. +6. **Save and deploy.** +7. Copy the Worker's URL — it looks like + `https://zeithawk-proxy..workers.dev`. + +## Point ZeitHawk at it + +In ZeitHawk's header, the CORS proxy section has a **Proxy endpoint** +field (below the on/off toggle), pre-filled with +`https://corsproxy.io/?url=`. Replace it with your Worker's URL plus +`/?url=`, e.g.: + +``` +https://zeithawk-proxy..workers.dev/?url= +``` + +That's stored in your browser's `localStorage`, so it persists across +visits until you change it again. + +## Caveats + +- **No allowlist, no auth.** Anyone who has this Worker's URL can use it + as an open relay to fetch anything from anywhere. That's the point (you + need it to reach arbitrary authorized-test targets), but don't reuse + this Worker for anything you wouldn't want acting as an open proxy. +- **It forwards headers and body as-is** — including anything you type + into Repeater/Intruder. Nothing about what you send through it is + logged by this code, but Cloudflare's own platform-level request logs + (if you have logging enabled on your account) would still see it, same + as any Worker you deploy. +- **Still bound by what the target itself allows.** If a target blocks + Cloudflare's IP ranges, rate-limits aggressively, or requires a + specific `User-Agent`, this proxy won't get around that — it's not a + general-purpose anti-blocking tool, just a CORS workaround. diff --git a/tools/zeithawk/cors-proxy-worker/worker.js b/tools/zeithawk/cors-proxy-worker/worker.js new file mode 100644 index 0000000..611235d --- /dev/null +++ b/tools/zeithawk/cors-proxy-worker/worker.js @@ -0,0 +1,101 @@ +/** + * A minimal CORS-bypassing reverse proxy for ZeitHawk, deployed as a + * Cloudflare Worker. Unlike a Google Apps Script web app (which can't set + * response headers at all, so a plain fetch() can never read its body + * cross-origin), a Worker can freely set Access-Control-Allow-Origin and + * Access-Control-Expose-Headers — which is the whole point here. + * + * Usage: GET /?url= + * Forwards method, headers, and body to the target, then returns the + * target's real status/headers/body with permissive CORS headers added. + * + * This has no allowlist or auth by design — it's meant to be pointed at + * whatever target you're authorized to test, the same as ZeitHawk itself. + * Anyone with the deployed URL can use it as an open proxy, so don't put + * anything sensitive in front of it you wouldn't want relayed. + */ + +const HOP_BY_HOP_REQUEST_HEADERS = [ + 'host', 'connection', 'keep-alive', 'transfer-encoding', 'upgrade', + 'te', 'trailer', 'proxy-authorization', 'proxy-connection', +]; + +export default { + async fetch(request) { + if (request.method === 'OPTIONS') { + return new Response(null, { status: 204, headers: corsHeaders() }); + } + + const reqUrl = new URL(request.url); + const target = reqUrl.searchParams.get('url'); + if (!target) { + return jsonError('Missing ?url= query parameter', 400); + } + + let targetUrl; + try { + targetUrl = new URL(target); + } catch (e) { + return jsonError('Invalid target URL: ' + target, 400); + } + if (targetUrl.protocol !== 'http:' && targetUrl.protocol !== 'https:') { + return jsonError('Only http/https targets are supported', 400); + } + + const forwardHeaders = new Headers(); + for (const [key, value] of request.headers) { + const k = key.toLowerCase(); + if (HOP_BY_HOP_REQUEST_HEADERS.includes(k)) continue; + if (k.startsWith('cf-') || k === 'x-forwarded-for' || k === 'x-real-ip') continue; + forwardHeaders.set(key, value); + } + + const init = { + method: request.method, + headers: forwardHeaders, + redirect: 'follow', + }; + if (!['GET', 'HEAD'].includes(request.method)) { + init.body = await request.arrayBuffer(); + } + + let targetRes; + try { + targetRes = await fetch(targetUrl.toString(), init); + } catch (e) { + return jsonError('Upstream request failed: ' + ((e && e.message) || String(e)), 502); + } + + const respHeaders = new Headers(targetRes.headers); + const exposeList = Array.from(respHeaders.keys()).join(', '); + respHeaders.set('Access-Control-Allow-Origin', '*'); + respHeaders.set('Access-Control-Expose-Headers', exposeList || '*'); + // Hop-by-hop, and content-encoding since fetch() already decoded the body. + respHeaders.delete('content-encoding'); + respHeaders.delete('content-length'); + respHeaders.delete('transfer-encoding'); + respHeaders.delete('connection'); + + return new Response(targetRes.body, { + status: targetRes.status, + statusText: targetRes.statusText, + headers: respHeaders, + }); + }, +}; + +function corsHeaders() { + return { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS', + 'Access-Control-Allow-Headers': '*', + 'Access-Control-Max-Age': '86400', + }; +} + +function jsonError(message, status) { + return new Response(JSON.stringify({ error: message }), { + status, + headers: Object.assign({ 'content-type': 'application/json' }, corsHeaders()), + }); +} diff --git a/tools/zeithawk/index.html b/tools/zeithawk/index.html index 4619ced..c5ca7cf 100644 --- a/tools/zeithawk/index.html +++ b/tools/zeithawk/index.html @@ -112,6 +112,7 @@ padding:12px 14px;font-size:13px;line-height:1.55;color:var(--ink);margin-top:10px; } .proxy-toggle code{background:var(--paper-raised);} + .proxy-base-row input{font-family:'IBM Plex Mono',monospace;font-size:12px;padding:7px 9px;} .switch{position:relative;display:inline-block;width:36px;height:20px;flex:0 0 auto;margin-top:1px;} .switch input{opacity:0;width:0;height:0;} .switch-track{position:absolute;inset:0;background:var(--line);border-radius:999px;cursor:pointer;transition:background 0.15s;} @@ -311,7 +312,12 @@

ZeitHawk

- CORS proxy: on — routes Repeater, Intruder, Recon's DNS/RDAP lookups, and Vuln Scan through the public proxy corsproxy.io so requests reach targets that would otherwise block cross-origin access. This sends the target URL — and anything you send it (headers, body, payloads) — through that third-party service, which you don't control. Turn this off for anything sensitive, or when testing a target that already sets its own permissive CORS headers. + CORS proxy: on — routes Repeater, Intruder, Recon's DNS/RDAP lookups, and Vuln Scan through a proxy so requests reach targets that would otherwise block cross-origin access. This sends the target URL — and anything you send it (headers, body, payloads) — through whatever endpoint is set below, which you don't control unless you deployed it yourself. Turn this off for anything sensitive, or when testing a target that already sets its own permissive CORS headers. +
+ + +

Default is the public corsproxy.io. See tools/zeithawk/cors-proxy-worker/ in the repo to deploy your own private proxy and paste its URL here instead.

+
@@ -677,33 +683,47 @@

Vuln Scan

} /* ------------------------------------------------------------------ * - * Public CORS proxy (opt-out) — routes requests through corsproxy.io so - * targets that don't set their own CORS headers can still be reached. - * A third-party service sees the target URL and whatever you send it, + * CORS proxy (opt-out, configurable endpoint) — routes requests through + * a proxy so targets that don't set their own CORS headers can still be + * reached. Defaults to the public corsproxy.io, but the endpoint can be + * pointed at a private proxy instead (see tools/zeithawk/cors-proxy-worker/). + * Whichever endpoint is set sees the target URL and whatever you send it, * so this is off-able for sensitive targets. Does NOT change the header- * visibility caveat in Vuln Scan: we only ever claim a header is present * when we actually read it, never infer "missing" from silence, since we - * can't verify what corsproxy.io itself exposes via CORS. + * can't verify what the configured proxy itself exposes via CORS. * ------------------------------------------------------------------ */ -const CORS_PROXY_BASE = 'https://corsproxy.io/?url='; +const DEFAULT_PROXY_BASE = 'https://corsproxy.io/?url='; function isProxyEnabled(){ try{ return localStorage.getItem('zeithawk_proxy_enabled') !== '0'; }catch(e){ return true; } } function setProxyEnabled(v){ try{ localStorage.setItem('zeithawk_proxy_enabled', v ? '1' : '0'); }catch(e){} } +function getProxyBase(){ + try{ return localStorage.getItem('zeithawk_proxy_base') || DEFAULT_PROXY_BASE; }catch(e){ return DEFAULT_PROXY_BASE; } +} +function setProxyBase(v){ + try{ localStorage.setItem('zeithawk_proxy_base', (v||'').trim() || DEFAULT_PROXY_BASE); }catch(e){} +} function viaProxy(url){ - return isProxyEnabled() ? (CORS_PROXY_BASE + encodeURIComponent(url)) : url; + return isProxyEnabled() ? (getProxyBase() + encodeURIComponent(url)) : url; } const proxyInput = document.getElementById('proxy-toggle-input'); const proxyStatus = document.getElementById('proxy-toggle-status'); +const proxyBaseInput = document.getElementById('proxy-base-input'); proxyInput.checked = isProxyEnabled(); proxyStatus.textContent = proxyInput.checked ? 'on' : 'off'; +proxyBaseInput.value = getProxyBase(); proxyInput.addEventListener('change', ()=>{ setProxyEnabled(proxyInput.checked); proxyStatus.textContent = proxyInput.checked ? 'on' : 'off'; }); +proxyBaseInput.addEventListener('change', ()=>{ + setProxyBase(proxyBaseInput.value); + proxyBaseInput.value = getProxyBase(); +}); // Fires a request from the browser and normalizes the outcome. Subject to // the same CORS/forbidden-header rules as any page — see the banner above. From 86c3b20141d8af1986e8173566b68821cab05bfd Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 05:23:27 +0000 Subject: [PATCH 2/4] Document that Claude Artifact previews can't do real network requests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Confirmed today: Vuln Scan (and everything else in ZeitHawk that makes outbound requests) fails with a generic "Failed to fetch" when run from a claude.ai artifact preview, regardless of target, CORS proxy, or proxy credentials — because the artifact sandbox's own CSP blocks fetch() to arbitrary third-party domains. The exact same requests work correctly once hosted as a real page (e.g. GitHub Pages). Adds a "Running it" section pointing at GitHub Pages as the way to actually use this tool. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SkupxCaLk9Nnq24YhigdAn --- tools/zeithawk/README.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tools/zeithawk/README.md b/tools/zeithawk/README.md index bc17e58..5160c0c 100644 --- a/tools/zeithawk/README.md +++ b/tools/zeithawk/README.md @@ -10,6 +10,23 @@ It is not part of the SecurityReview Rails application and has no runtime dependency on it — it lives here as a companion tool for people doing manual review/testing work against systems they're authorized to test. +## Running it + +Host `index.html` as a static file (e.g. via [GitHub +Pages](https://github.com/BruceLittle/SecurityReview/settings/pages) — +Source: "Deploy from a branch", branch `main`, folder `/(root)` — it'll be +served at `https://.github.io//tools/zeithawk/`) or open the +file directly in a browser. + +**Don't rely on a Claude Artifact preview of this file for real use.** +Claude's artifact sandbox blocks outbound `fetch()` to arbitrary +third-party domains via its own CSP — every request in this app +(Repeater, Intruder, Recon, Vuln Scan, and any CORS proxy) will fail with +a generic `Failed to fetch` there regardless of target, proxy +configuration, or anything else in this code. That's a property of the +artifact wrapper, not a bug here — confirmed by the exact same requests +working correctly once run from a real hosted page instead. + ## Tools - **Repeater** — build one HTTP request (method/URL/headers/body), send it, From c0ddd7f80112dc097da5b22ff097517d7914fdb5 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 05:28:44 +0000 Subject: [PATCH 3/4] Fall back to the CORS proxy instead of forcing it for every request MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recon's DNS lookups (dns.google) and RDAP lookups (rdap.org) already worked without a proxy in many cases, since dns.google sends its own permissive CORS headers. Force-routing every request through the configured proxy (added when the CORS-proxy toggle was introduced) meant a single misbehaving or misconfigured proxy endpoint could break lookups that used to work fine unproxied — this is what broke Recon. fetchDirectOrProxied() now tries a direct fetch first and only falls back to the proxy if that fails outright, applied consistently across Repeater/Intruder, Recon's DNS/RDAP lookups, and Vuln Scan. Targets that already allow cross-origin access keep working with zero dependency on a third party; targets that need the proxy still get it on the retry. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SkupxCaLk9Nnq24YhigdAn --- tools/zeithawk/index.html | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/tools/zeithawk/index.html b/tools/zeithawk/index.html index c5ca7cf..4690e46 100644 --- a/tools/zeithawk/index.html +++ b/tools/zeithawk/index.html @@ -710,6 +710,20 @@

Vuln Scan

function viaProxy(url){ return isProxyEnabled() ? (getProxyBase() + encodeURIComponent(url)) : url; } +// Tries a direct fetch first and only falls back to the configured proxy +// if that outright fails. Targets that already send their own permissive +// CORS headers (dns.google, for one) keep working with no dependency on +// a third party at all; targets that need the proxy still get it. This +// also means a proxy outage/misbehavior never breaks something that +// worked fine unproxied before the proxy toggle existed. +async function fetchDirectOrProxied(url, opts){ + if(!isProxyEnabled()) return fetch(url, opts); + try{ + return await fetch(url, opts); + }catch(directErr){ + return fetch(viaProxy(url), opts); + } +} const proxyInput = document.getElementById('proxy-toggle-input'); const proxyStatus = document.getElementById('proxy-toggle-status'); const proxyBaseInput = document.getElementById('proxy-base-input'); @@ -739,7 +753,7 @@

Vuln Scan

if(body && !['GET','HEAD'].includes(method)) opts.body = body; const t0 = performance.now(); try{ - const res = await fetch(viaProxy(url), opts); + const res = await fetchDirectOrProxied(url, opts); const text = await res.text(); const ms = Math.round(performance.now() - t0); const outHeaders = []; @@ -1245,7 +1259,7 @@

Vuln Scan

async function dohQuery(name, type){ const url = 'https://dns.google/resolve?name=' + encodeURIComponent(name) + '&type=' + encodeURIComponent(type); try{ - const res = await fetch(viaProxy(url), { signal: AbortSignal.timeout(6000) }); + const res = await fetchDirectOrProxied(url, { signal: AbortSignal.timeout(6000) }); if(!res.ok) return { ok:false, error: 'HTTP '+res.status }; const data = await res.json(); return { ok:true, data }; @@ -1257,7 +1271,7 @@

Vuln Scan

async function rdapLookup(target, isIp){ const url = 'https://rdap.org/' + (isIp ? 'ip/' : 'domain/') + encodeURIComponent(target); try{ - const res = await fetch(viaProxy(url), { headers:{accept:'application/rdap+json'}, signal: AbortSignal.timeout(8000) }); + const res = await fetchDirectOrProxied(url, { headers:{accept:'application/rdap+json'}, signal: AbortSignal.timeout(8000) }); if(!res.ok) return { ok:false, error:'HTTP '+res.status }; const data = await res.json(); return { ok:true, data }; @@ -1503,7 +1517,7 @@

Vuln Scan

let mainRes = null, mainErr = null, bodyText = ''; try{ - mainRes = await fetch(viaProxy(url), { mode:'cors', signal: AbortSignal.timeout(8000) }); + mainRes = await fetchDirectOrProxied(url, { mode:'cors', signal: AbortSignal.timeout(8000) }); bodyText = await mainRes.text(); }catch(e){ mainErr = (e && e.message) || String(e); } @@ -1555,7 +1569,7 @@

Vuln Scan

const pathResults = await Promise.all(PATH_CHECKS.map(async pc=>{ try{ - const res = await fetch(viaProxy(origin + pc.path), { mode:'cors', signal: AbortSignal.timeout(5000) }); + const res = await fetchDirectOrProxied(origin + pc.path, { mode:'cors', signal: AbortSignal.timeout(5000) }); return Object.assign({}, pc, { status: res.status }); }catch(e){ return Object.assign({}, pc, { status: null }); } })); From 3c26f3a14e9263f28d4391f5e460a9bdbdd2d63a Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 17 Sep 2026 05:42:54 +0000 Subject: [PATCH 4/4] Stop one bad DNS record type from silently dropping the rest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Recon runs 7 DNS-record-type queries in parallel and processes the results in a single forEach. An unexpected (non-array) Answer shape for any one record type threw inside that loop, which silently killed processing for every type after it in the array — NS, CNAME, and SOA disappeared even though A/AAAA/MX/TXT (processed first) rendered fine. This is what caused zeitview.com's real NS records to show as "not found" despite the underlying DNS data being correct. Each record type's result is now handled in its own try/catch with an explicit Array.isArray check, so one malformed response is skipped without affecting the rest. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01SkupxCaLk9Nnq24YhigdAn --- tools/zeithawk/index.html | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/tools/zeithawk/index.html b/tools/zeithawk/index.html index 4690e46..07868cb 100644 --- a/tools/zeithawk/index.html +++ b/tools/zeithawk/index.html @@ -1391,11 +1391,16 @@

Vuln Scan

const resolvedIps = []; results.forEach((r, i)=>{ const t = types[i]; - if(!r.ok || !r.data.Answer) return; - r.data.Answer.forEach(ans=>{ - rows.push([t, ans.data, ans.TTL]); - if(t === 'A' || t === 'AAAA') resolvedIps.push(ans.data); - }); + // A malformed/unexpected response for one record type must never + // stop the rest of forEach's iterations (types after it in the + // array — NS, CNAME, SOA — would otherwise silently disappear). + try{ + if(!r.ok || !Array.isArray(r.data.Answer)) return; + r.data.Answer.forEach(ans=>{ + rows.push([t, ans.data, ans.TTL]); + if(t === 'A' || t === 'AAAA') resolvedIps.push(ans.data); + }); + }catch(e){ /* skip this record type, keep processing the rest */ } }); document.getElementById('recon-dns-tbody').innerHTML = rows.length ? rows.map(r=>''+escapeHtml(r[0])+''+escapeHtml(r[1])+''+escapeHtml(r[2])+'').join('')