What We’re Testing
QuickZTNA integrates with external threat intelligence providers (AbuseIPDB, VirusTotal, CrowdStrike) to score IP addresses. The system has two layers:
- CLI:
ztna threat check <ip>— auto-detects the target as an IP, sends aPOST /api/threat-checkrequest, and displays a verdict with a 0-100 confidence score - API:
POST /api/threat-check— acceptsorg_id,ip_address, and optionalmachine_id; iterates over all enabledthreat_intel_configsrows for the org; queries each provider; records results in thethreat_checkstable; and returns a combined verdict
The backend handler (handlers/threat-check.ts) performs these steps for each enabled provider:
- Decrypts the stored
api_key_encryptedusing the server-side encryption key - Calls the provider API (AbuseIPDB
/api/v2/check, VirusTotal/api/v3/ip-addresses/{ip}, or CrowdStrike Falcon Intel via OAuth2) - Normalises the response to
{ score, verdict, raw }where verdict isclean,suspicious, ormalicious - Compares the score to the provider’s
block_threshold(default 50) to decideblockedvsallowed - Inserts a row into
threat_checkswith the verdict, confidence score, raw response, and action taken
The CLI command (cmd_threat.go) auto-detects the target type: if net.ParseIP succeeds, it is classified as ip.
Your Test Setup
| Machine | Role |
|---|---|
| ⊞ Win-A | Browser (dashboard) + CLI testing |
| 🐧 Linux-C | CLI testing from a Linux endpoint |
Prerequisites:
- At least one threat intel provider (AbuseIPDB, VirusTotal, or CrowdStrike) configured and enabled on the Threat Intelligence page (
/threat-intel) with a valid API key - The machine must be authenticated (
ztna uprunning, or a valid JWT for API calls)
ST1 — CLI IP Reputation Check (Known Safe IP)
What it verifies: ztna threat check correctly queries providers for a benign IP and returns a low score with a clean verdict.
Steps:
- On ⊞ Win-A , ensure the tunnel is up:
ztna status
Confirm Authenticated: true and an org is connected.
- Check a well-known safe IP (Google Public DNS):
ztna threat check 8.8.8.8
Expected output:
Target: 8.8.8.8 (ip)
Verdict: CLEAN
Score: 0/100
Provider Results:
──────────────────────────────────────────────────
abuseipdb clean
virustotal clean
The exact providers listed depend on which are configured for the org. The score should be low (under 25) and the verdict should be CLEAN.
- Repeat with JSON output to inspect the raw structure:
ztna threat check 8.8.8.8 --json
Expected JSON shape:
{
"target": "8.8.8.8",
"target_type": "ip",
"verdict": "clean",
"score": 0,
"providers": [
{
"name": "abuseipdb",
"verdict": "clean",
"details": ""
}
]
}
Pass: Verdict is CLEAN, score is under 25, at least one provider returned results.
Fail / Common issues:
- “not connected to an organization” — run
ztna upfirst - “error checking threat” — the API returned an error; check that the provider API key is valid on the
/threat-inteldashboard page - No providers listed — no
threat_intel_configsrows are enabled for the org
ST2 — CLI IP Reputation Check (Known Malicious IP)
What it verifies: A known-bad IP returns a high score and a malicious or suspicious verdict.
Steps:
- On 🐧 Linux-C , check an IP from a public threat test list (the AbuseIPDB “most reported” list frequently includes
185.220.101.1or similar Tor exit nodes):
ztna threat check 185.220.101.1
Expected output (approximate):
Target: 185.220.101.1 (ip)
Verdict: MALICIOUS
Score: 87/100
Provider Results:
──────────────────────────────────────────────────
abuseipdb malicious (abuseConfidenceScore: 100)
virustotal suspicious (3 malicious detections)
The score depends on provider data at test time. The key assertion is that the score exceeds 50 and the verdict is either SUSPICIOUS or MALICIOUS.
- Verify the check was recorded in the database by calling the CRUD API:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://login.quickztna.com/api/db/threat_checks?org_id=eq.$ORG_ID&ip_address=eq.185.220.101.1" | jq '.data[0]'
Expected fields in the response object:
ip_address:"185.220.101.1"verdict:"malicious"or"suspicious"confidence_score: a number greater than 50action_taken:"blocked"(if score exceeds the provider’sblock_threshold) or"allowed"raw_response: a JSON object containing the provider’s raw data
Pass: Score exceeds 50, verdict is SUSPICIOUS or MALICIOUS, and a threat_checks row was created.
Fail / Common issues:
- Score is 0 with verdict
clean— the IP may have been delisted. Try a different known-bad IP from AbuseIPDB’s public reports. action_takenisalloweddespite high score — theblock_thresholdon thethreat_intel_configsrow may be set higher than the returned score. Check the threshold on the/threat-intelpage.
ST3 — API Direct IP Check (POST /api/threat-check)
What it verifies: The API endpoint accepts a direct IP check request and returns the correct envelope structure.
Steps:
- From ⊞ Win-A , call the API directly:
$body = @{
org_id = "$ORG_ID"
ip_address = "1.1.1.1"
} | ConvertTo-Json
Invoke-RestMethod -Uri "https://login.quickztna.com/api/threat-check" `
-Method POST `
-Headers @{ Authorization = "Bearer $TOKEN"; "Content-Type" = "application/json" } `
-Body $body
Expected response:
{
"success": true,
"data": {
"checked": true,
"blocked": false,
"results": [
{
"provider": "abuseipdb",
"verdict": "clean",
"score": 0,
"action": "allowed"
}
]
},
"error": null
}
-
Verify the
data.checkedfield istrueanddata.resultsis an array with one entry per enabled provider. -
Verify the
data.blockedfield isfalsefor a clean IP (it becomestruewhen any provider’s score exceeds itsblock_threshold).
Pass: success: true, checked: true, blocked: false, and at least one provider result with verdict clean.
Fail / Common issues:
"checked": false, "message": "No threat intel providers configured"— no providers are enabled. Configure one on the/threat-intelpage.401 UNAUTHORIZED— JWT expired or missing. Re-authenticate.403 FORBIDDEN— the user is not a member of the specifiedorg_id.
ST4 — IP Check With machine_id Association
What it verifies: When a machine_id is provided, the threat_checks record is linked to that machine, which feeds into the risk engine’s threat score calculation.
Steps:
- Get a machine ID from the dashboard or API:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://login.quickztna.com/api/db/machines?org_id=eq.$ORG_ID&status=eq.online" | jq '.data[0].id'
- Run a threat check with the machine_id:
curl -s -X POST "https://login.quickztna.com/api/threat-check" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"org_id\": \"$ORG_ID\", \"ip_address\": \"185.220.101.1\", \"machine_id\": \"$MACHINE_ID\"}" | jq
- Verify the recorded threat_check has the machine_id:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://login.quickztna.com/api/db/threat_checks?org_id=eq.$ORG_ID&machine_id=eq.$MACHINE_ID" \
| jq '.data | length'
The count should be at least 1.
- The risk engine (
risk-engine.ts) usesthreat_checksrows with a matchingmachine_idfrom the last 24 hours to calculate the threat component (20% weight) of the machine’s overall risk score. Trigger a heartbeat (wait up to 60 seconds) or manually check:
curl -s -H "Authorization: Bearer $TOKEN" \
"https://login.quickztna.com/api/db/risk_scores?machine_id=eq.$MACHINE_ID" | jq '.data[0] | {overall_score, risk_level, threat_score}'
Pass: The threat_checks row has the correct machine_id, and the risk_scores row shows a non-zero threat_score.
Fail / Common issues:
machine_idisnullin the threat_checks row — the machine_id was not included in the request bodyrisk_scoresrow showsthreat_score: 0— the heartbeat has not fired since the check; wait for the next heartbeat cycle
ST5 — IP Check With No Providers Configured
What it verifies: When no threat intel providers are configured or enabled for an org, the API returns a graceful “not configured” response instead of an error.
Steps:
- If testing in a separate org with no providers, call:
curl -s -X POST "https://login.quickztna.com/api/threat-check" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "{\"org_id\": \"$ORG_ID_NO_PROVIDERS\", \"ip_address\": \"8.8.8.8\"}" | jq
Expected response:
{
"success": true,
"data": {
"checked": false,
"message": "No threat intel providers configured"
},
"error": null
}
- Alternatively, temporarily disable all providers on the
/threat-intelpage (toggle each provider’s Enabled switch off), then repeat the check.
Pass: success: true, checked: false, message says “No threat intel providers configured”.
Fail / Common issues:
- Returns
success: falseor a 500 error — this would indicate a backend bug; the handler should gracefully handle the empty config case
Summary
| Sub-test | What it proves | Key assertion |
|---|---|---|
| ST1 | CLI check of safe IP | Score under 25, verdict CLEAN |
| ST2 | CLI check of malicious IP | Score over 50, verdict SUSPICIOUS or MALICIOUS, row in threat_checks |
| ST3 | Direct API call | Envelope: checked: true, blocked: false, provider results array |
| ST4 | machine_id association | threat_checks row linked to machine, feeds risk engine |
| ST5 | No providers configured | Graceful checked: false response, no error |