pancloud-nodejs is the Palo Alto Networks Application Framework SDK for
Node.js. It is a TypeScript library that wraps the three Cortex Data Lake
services that PAN exposes to third-party tooling — Logging Service,
Event Service, and Directory Sync Service — and adds the OAuth2
plumbing, automatic access-token refresh, and retry logic you would
otherwise have to write yourself.
This guide is for the developer building a Node.js application that needs
to read PAN-OS firewall logs, subscribe to log events, or query Active
Directory data out of a Cortex Data Lake instance. It is organized as a
linear path from the first npm install through credential setup,
day-to-day querying patterns, and the failure modes you will hit in
production.
1. Quick Start
The fastest way to talk to a Cortex Data Lake from Node.js: install the
package, set three environment variables, and call autoCredentials().
The SDK figures out the credential type from the variables you set, hands
you back a refresh-aware Credentials object, and you are ready to query.
Install
npm install pancloud-nodejs
Tip
The package is published as
pancloudon itspackage.jsonnamefield but is consumed under the GitHub repository name. The TypeScript test suite imports it asfrom 'pancloud-nodejs'and that is the import path shown throughout this guide.
A 5-line query
Save this as quickstart.ts:
import { autoCredentials, LoggingService } from 'pancloud-nodejs'
async function main(): Promise<void> {
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
const job = await ls.query({
query: "SELECT * FROM panw.traffic LIMIT 10",
startTime: Math.floor(Date.now() / 1000) - 3600,
endTime: Math.floor(Date.now() / 1000)
})
console.log(`Query ${job.queryId}: ${job.queryStatus}`)
}
main().catch(e => console.error(e))
Provide credentials via environment variables (the simplest path):
export PAN_CLIENT_ID="<your-oauth2-client-id>"
export PAN_CLIENT_SECRET="<your-oauth2-client-secret>"
export PAN_REFRESH_TOKEN="<your-refresh-token>"
export PAN_ENTRYPOINT="https://api.us.paloaltonetworks.com"
node --loader ts-node/esm quickstart.ts
Expected output:
PANCLOUD: {"source":"AutoCredentials","message":"Using memory based credentials provider"}
PANCLOUD: {"source":"defaultCredentialsFactory","message":"Got 'client_id'"}
PANCLOUD: {"source":"defaultCredentialsFactory","message":"Got 'client_secret'"}
PANCLOUD: {"source":"LoggingService","message":"Creating new LoggingService object for entryPoint https://api.us.paloaltonetworks.com"}
Query 1234abcd-...: RUNNING
Note
The query returns a
JobResultthat may be inRUNNING,FINISHED,JOB_FINISHED,JOB_FAILED, orCANCELLEDstate. For the synchronous result, seeLoggingServiceClient(the streaming wrapper) or the auto-pollcallBackpattern shown later in this guide.
What you get
- OAuth2 management — the SDK refreshes the access token automatically
when it is within 5 minutes of expiry (
accTokenGuardTimedefault). - Three service clients —
LoggingService,EventService,DirectorySyncService, all reached via afactory(cred)static method. - Retry-with-backoff — every HTTP call is wrapped in a
retrier(default 3 attempts, 100 ms apart). - Async log streaming —
LoggingServiceClientis a NodeReadablestream that yields log records as they become available. - Multi-datalake / multi-tenant —
CortexHubHelperandCortexCredentialProvidercover the OAuth2 authorization-code flow plus per-tenant credential storage.
What to try next
- Section 2 walks through the supported install methods.
- Section 3 covers the four credential strategies the SDK ships with.
- Section 4 has the full surface area — every public class and its main methods.
- Section 5 has copy-paste recipes for the eight workflows most apps need.
2. Installation
Requirements
| Component | Minimum | Notes |
|---|---|---|
| Node.js | 8.x or later | TypeScript compile target is es2017 (tsconfig.json); native async/await and Symbol.iterator are required. |
| TypeScript | 2.7+ | Optional. The package ships JavaScript built into dist/ and TypeScript declarations alongside. |
| OS | Linux, macOS, Windows | Pure JavaScript with https, crypto, fs, stream, events, url, querystring from the Node standard library. No native modules. |
| Network | Outbound HTTPS to *.paloaltonetworks.com |
The SDK calls https://api.us.paloaltonetworks.com (Americas) or https://api.eu.paloaltonetworks.com (Europe), plus https://api.paloaltonetworks.com for IDP. |
Important
You need OAuth2 credentials issued by Palo Alto Networks before any of this works. That means a registered application in the Cortex hub plus a
client_id, aclient_secret, and either arefresh_token(for long-lived headless usage) or a developer token (for quick testing). The SDK does not issue credentials; it only consumes them.
Install methods
npm
npm install pancloud-nodejs
yarn
yarn add pancloud-nodejs
pnpm
pnpm add pancloud-nodejs
From source
git clone https://github.com/PaloAltoNetworks/pancloud-nodejs.git
cd pancloud-nodejs
npm run build:library
The build:library script empties dist/, runs tsc -p tsconfig.json,
and emits CommonJS modules with declaration files alongside.
Verify the install
Once installed, this one-liner confirms the import path works:
node -e "console.log(Object.keys(require('pancloud-nodejs')).slice(0, 6))"
Expected output:
[
'defaultCredentialsFactory',
'DevTokenCredentials',
'autoCredentials',
'LoggingService',
'LoggingServiceClient',
'EventService'
]
Note
The package's
package.jsondeclares"main": "dist/index.js"and has an emptydependenciesblock — every external API is satisfied by the Node standard library. Your bundle gets no transitive runtime dependencies from this SDK.
TypeScript projects
For TypeScript consumers, no @types/* package is required — type
declarations ship in the package. The tsconfig.json recommended for an
SDK consumer matches the test suite shipped with this repo:
{
"compilerOptions": {
"noImplicitAny": true,
"strictNullChecks": true,
"target": "es2017",
"module": "commonjs"
}
}
Warning
The
Symbol.iteratorpattern thatEventServiceexposes (for (let p of es) { ... }) requires an iteration target ofes2015or later. If you settarget: "es5"you must also enable"downlevelIteration": trueto get the same behaviour.
3. Configuration
The only thing the SDK has to be told is how to get an access token. Everything else (retry counts, timeouts, log level, channel ID) has a sensible default.
The four credential strategies
The SDK ships four ways to produce a Credentials object. Pick the one
that matches your deployment shape:
| Strategy | Factory | Use when |
|---|---|---|
| Auto | autoCredentials() |
Hands-off setup. SDK reads env vars and picks the best path. |
| Memory provider (refresh token) | defaultCredentialsProviderFactory() |
Long-lived headless service, refresh token in memory only. |
| Filesystem provider | fsCredentialsFactory() |
Headless service with persistent encrypted credential cache for multiple data lakes. |
| Developer token | new DevTokenCredentials() |
Quick scripts, demos, the apiexplorer.rocks developer token flow. |
| Static | defaultCredentialsFactory() |
Tests where you have a still-valid access_token in hand. No refresh. |
Environment variables
autoCredentials() and the two factory functions read from process.env.
The names are fixed:
| Variable | Read by | Purpose |
|---|---|---|
PAN_CLIENT_ID |
memory + filesystem providers, autoCredentials |
OAuth2 client_id |
PAN_CLIENT_SECRET |
memory + filesystem providers, autoCredentials |
OAuth2 client_secret |
PAN_REFRESH_TOKEN |
memory provider, autoCredentials |
OAuth2 refresh_token |
PAN_ACCESS_TOKEN |
autoCredentials (static fallback) |
Pre-fetched JWT access token |
PAN_ENTRYPOINT |
all credential paths | Regional API endpoint (https://api.us.paloaltonetworks.com or https://api.eu.paloaltonetworks.com) |
PAN_DEVELOPER_TOKEN |
DevTokenCredentials |
Token issued by the developer-token provider |
PAN_DEVELOPER_TOKEN_PROVIDER |
DevTokenCredentials |
Override the default https://app.apiexplorer.rocks/request_token URL |
Important
Treat
PAN_CLIENT_SECRETandPAN_REFRESH_TOKENas long-lived secrets. A leaked refresh token means full access to your Cortex Data Lake until someone manually revokes it. Store them in a vault, inject at runtime, never commit to source control.
The two regional entry points
The SDK exports a string-literal type EntryPoint with exactly two
allowed values:
export type EntryPoint =
'https://api.eu.paloaltonetworks.com' |
'https://api.us.paloaltonetworks.com'
A convenience map region2EntryPoint translates 'americas' and
'europe' to the corresponding URLs.
Tip
If you set
PAN_ENTRYPOINTto anything else, the SDK still accepts it (it castsstringtoEntryPoint) but you have left the supported matrix. Stick to one of the two literal values unless PAN issues a new regional API endpoint.
Common knobs (all optional)
Both EventService.factory() and LoggingService.factory() accept the
same EmitterOptions configuration object, which extends CoreOptions:
| Option | Type | Default | Description |
|---|---|---|---|
level |
LogLevel |
LogLevel.INFO |
Minimum severity that the SDK's internal logger prints. |
retrierCount |
number |
3 |
Attempts per HTTP call before failing. |
retrierDelay |
number (ms) |
100 |
Delay between retry attempts. |
fetchTimeout |
number (ms) |
unset | Per-request timeout passed to the underlying https request. |
allowDup |
boolean |
false |
Allow registering the same event listener multiple times. |
l2Corr |
object |
unset | Enable the L2/L3 correlation engine (Event Service only). |
channelId |
string |
"EventFilter" |
Event Service channel ID (EventService only). |
autoPollSleep |
number (ms) |
200 |
Delay between auto-poll iterations. |
For everything else, see the Configuration Reference in the Admin Guide.
4. Core Usage
This section walks through the four pieces every application touches: credentials, the three service clients, and the iteration patterns the clients support.
4.1 Credentials
Every service factory takes a Credentials instance. Credentials is
abstract — the SDK ships four concrete subclasses plus a factory for
each.
autoCredentials() — let the SDK pick
The simplest entry point. It inspects environment variables and chooses:
- If
PAN_CLIENT_ID+PAN_CLIENT_SECRET+PAN_REFRESH_TOKENare all set, it uses the memory provider (refresh-token flow). - Else if
PAN_ACCESS_TOKENis set, it builds a static credentials object (no refresh). - Else it falls back to developer token mode and reads
PAN_DEVELOPER_TOKEN.
import { autoCredentials } from 'pancloud-nodejs'
const cred = await autoCredentials()
// cred is now a Credentials instance ready to hand to any factory
You can override individual fields without setting environment variables:
const cred = await autoCredentials({
accessToken: '<jwt>',
entryPoint: 'https://api.eu.paloaltonetworks.com'
})
defaultCredentialsProviderFactory() — explicit memory provider
When you want the refresh-token flow but do not want it picked from env:
import { defaultCredentialsProviderFactory } from 'pancloud-nodejs'
const cred = await defaultCredentialsProviderFactory({
clientId: '<oauth2-client-id>',
clientSecret: '<oauth2-client-secret>',
refreshToken: '<oauth2-refresh-token>',
entryPoint: 'https://api.us.paloaltonetworks.com'
})
Internally this returns a Credentials bound to a one-datalake
DefaultCredentialsProvider instance with the fixed datalake ID
'DEFAULT'.
fsCredentialsFactory() — encrypted file cache
For headless services that want refresh tokens persisted across restarts.
Tokens are stored in ${PAN_PREFIX}_PANCLOUD_CONFIG.json (default
PAN_PANCLOUD_CONFIG.json) encrypted with aes128 derived from the
secret you provide:
import { fsCredentialsFactory } from 'pancloud-nodejs'
const provider = await fsCredentialsFactory({
clientId: process.env.PAN_CLIENT_ID!,
clientSecret: process.env.PAN_CLIENT_SECRET!,
secret: process.env.PAN_FS_SECRET! // encryption key (any string)
})
const cred = await provider.registerManualDatalake(
'datalake-1',
'https://api.us.paloaltonetworks.com',
'<refresh-token>'
)
DevTokenCredentials — quick demos
Useful with the apiexplorer.rocks flow if you have a developer token:
import { DevTokenCredentials } from 'pancloud-nodejs'
const cred = new DevTokenCredentials({
developerToken: process.env.PAN_DEVELOPER_TOKEN,
entryPoint: 'https://api.us.paloaltonetworks.com'
})
await cred.retrieveAccessToken()
defaultCredentialsFactory() — static, no refresh
For tests where you already have a fresh JWT in hand:
import { defaultCredentialsFactory } from 'pancloud-nodejs'
const cred = defaultCredentialsFactory(
'https://api.us.paloaltonetworks.com',
'<eyJ...JWT...>'
)
Warning
defaultCredentialsFactory()returns aStaticCredentialsinstance. ItsretrieveAccessToken()is a no-op — when the JWT expires, queries will start failing with 401 errors. Use it for fixtures only.
Automatic refresh
Every concrete Credentials subclass implements autoRefresh(). The
service clients call it before every HTTP request via
CoreClass.checkAutoRefresh(). If the access token is within
guardTime (5 minutes by default) of expiry, the SDK calls
retrieveAccessToken() and updates the Authorization header
transparently.
// You almost never call these directly; the service clients do.
await cred.getAccessToken() // Returns the current access token
await cred.getExpiration() // Returns the unix epoch validity
await cred.autoRefresh() // Refreshes if needed, returns validUntil
cred.getEntryPoint() // Returns the regional entry point
4.2 LoggingService — the main query API
LoggingService runs SQL-style queries against the Cortex Data Lake's log
indexes (panw.traffic, panw.threat, panw.system, etc.) and returns
results in batches.
Synchronous query (poll-yourself)
import { autoCredentials, LoggingService } from 'pancloud-nodejs'
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
const job = await ls.query({
query: "SELECT * FROM panw.threat WHERE risk-of-app > 3",
startTime: 1577836800,
endTime: 1580515200,
maxWaitTime: 30000
})
console.log(`queryId: ${job.queryId}, status: ${job.queryStatus}`)
// If the result didn't arrive on the first call, poll until done:
let next = job
while (next.queryStatus !== 'JOB_FINISHED' && next.queryStatus !== 'JOB_FAILED') {
next = await ls.poll(next.queryId, next.sequenceNo + 1, 30000)
}
Asynchronous query with callback (the recommended pattern)
When you provide a callBack.event handler, the SDK starts an
auto-poller in the background. Every time a sequence of results is ready,
your handler is called with an EmitterInterface<any[]> containing the
records.
import { autoCredentials, LoggingService, EmitterInterface } from 'pancloud-nodejs'
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
const completed = await ls.query({
query: "SELECT * FROM panw.traffic",
startTime: Math.floor(Date.now() / 1000) - 3600,
endTime: Math.floor(Date.now() / 1000),
logType: 'panw.traffic',
callBack: {
event: (e: EmitterInterface<any[]>) => {
if (e.message) {
console.log(`Got ${e.message.length} ${e.logType} records`)
}
}
}
})
console.log(`Job ${completed.queryId} done, ${ls.getLsStats().records} records total`)
Streaming query with LoggingServiceClient
LoggingServiceClient extends Node's Readable stream interface. It
lazy-starts the query on the first _read() and pushes each batch as a
single chunk in object mode:
import { autoCredentials, LoggingService, LoggingServiceClient } from 'pancloud-nodejs'
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
const stream = new LoggingServiceClient(ls, {
query: "SELECT * FROM panw.traffic",
startTime: Math.floor(Date.now() / 1000) - 3600,
endTime: Math.floor(Date.now() / 1000),
retries: 20, // attempts before giving up on RUNNING state
delay: 200 // ms between RUNNING-state polls
})
stream.on('data', (records: any[]) => {
console.log(`stream chunk: ${records.length} records`)
})
stream.on('end', () => console.log('stream finished'))
stream.on('error', e => console.error('stream error:', e))
Job statuses
Every result carries a queryStatus field:
| Status | Meaning |
|---|---|
RUNNING |
Job dispatched, no results yet. Keep polling. |
FINISHED |
This sequence delivered; more sequences are coming. Increment sequenceNo and poll again. |
JOB_FINISHED |
All sequences delivered; you are done. |
JOB_FAILED |
Server-side failure. Record the queryId and stop polling. |
CANCELLED |
Caller invoked cancelPoll() or deleteQuery(). |
Other methods
| Method | What it does |
|---|---|
ls.poll(qid, seqNo, maxWaitTime?) |
Fetch the next sequence of a running query. |
ls.deleteQuery(queryId) |
Cancel a query on the server side. |
ls.cancelPoll(queryId, err?) |
Remove a query from the auto-poll queue and call deleteQuery. |
ls.write(vendorName, logType, data[]) |
Push log records into the service. Requires registered vendor + log type. |
ls.getLsStats() |
Return per-instance counters: queries, polls, deletes, records, writes, plus inherited Emitter stats. |
4.3 EventService — the streaming subscription API
Where LoggingService runs ad-hoc SQL, EventService opens a long-lived
channel (default name EventFilter) into which the Application
Framework publishes matching events as they happen. Your code applies a
filter to that channel — a list of tables and optional WHERE clauses
— and then polls for events.
Set a filter and poll synchronously
import { autoCredentials, EventService, EsFilterBuilderCfg } from 'pancloud-nodejs'
const builderCfg: EsFilterBuilderCfg = {
filter: [
{ table: "panw.traffic", timeout: 1000, batchSize: 8000 },
{ table: "panw.dpi", timeout: 1000, batchSize: 8000 },
{ table: "panw.threat", where: 'where risk-of-app > 3' }
],
flush: false,
filterOptions: {}
}
const cred = await autoCredentials()
const es = EventService.factory(cred)
await es.filterBuilder(builderCfg)
const events = await es.poll()
events.forEach(e => {
console.log(`Event Type: ${e.logType}, Record Count: ${e.event.length}`)
})
Set a filter and stream asynchronously
When you provide a callBack in filterOptions, the SDK auto-polls and
pushes each event batch through the handler:
const builderCfg: EsFilterBuilderCfg = {
filter: [
{ table: "panw.traffic", timeout: 1000 },
{ table: "panw.threat", where: 'where risk-of-app > 3' }
],
filterOptions: {
callBack: {
event: e => {
if (e.message) console.log(`got ${e.message.length} ${e.logType} events`)
}
},
poolOptions: { ack: true, pollTimeout: 1000 }
}
}
const es = EventService.factory(cred)
await es.filterBuilder(builderCfg)
// Auto-poll starts in the background; let it run for 60 seconds:
await new Promise<void>(resolve => setTimeout(resolve, 60000))
es.pause()
await es.clearFilter(true)
console.log(JSON.stringify(es.getEsStats(), undefined, ' '))
Iterating without a callback
EventService implements Iterable<Promise<EsEvent[]>>. You can drive
the polling loop directly with for...of:
const es = EventService.factory(cred)
await es.filterBuilder(builderCfg)
let iterations = 10
for (const promise of es) {
if (iterations-- === 0) break
const response = await promise
response.forEach(e => console.log(`${e.event.length} ${e.logType} events`))
}
await es.clearFilter()
Channel control methods
| Method | Description |
|---|---|
es.filterBuilder(cfg) |
Build and apply a filter from a friendly config. |
es.setFilters(cfg) |
Lower-level: apply an EsFilterCfg directly. |
es.getFilters() |
Return the current filter set. |
es.clearFilter(flush?) |
Replace the filter with an empty one (and optionally flush). |
es.poll() |
Fetch one batch of events. |
es.ack() |
Acknowledge the current batch (mark as consumed). |
es.nack() |
Negative acknowledge — leave the batch on the channel. |
es.flush() |
Discard everything currently buffered server-side. |
es.pause() |
Stop the auto-poll loop without changing the filter. |
es.resume() |
Restart auto-poll after pause(). |
es.getEsStats() |
Per-instance counters: polls, acks, nacks, flushes, filtersets, filtergets. |
Warning
The Event Service rejects the
tms.analytics,tms.config,tms.system, andtms.threatlog types — onlytms.trapsis allowed from thetms.*namespace.filterBuilder()validates this before the HTTP call and throwsPanCloudError(CONFIG)if you pass an invalid table.
4.4 DirectorySyncService — Active Directory data
DirectorySyncService exposes the directory inventory (computers,
containers, groups, organizational units, users) that the
PAN-OS-integrated Directory Sync agent has uploaded.
import { autoCredentials, DirectorySyncService } from 'pancloud-nodejs'
const cred = await autoCredentials()
const dss = await DirectorySyncService.factory(cred)
const domains = await dss.domains()
console.log(`${domains.length} domains synced`)
const userCount = await dss.count(domains[0].dns, 'users')
console.log(`${userCount} users in ${domains[0].dns}`)
const userPage = await dss.query('users', {
domain: domains[0].dns,
pageNumber: 1,
pageSize: 100
})
console.log(`page 1 has ${userPage.result.length} entries`)
The five object classes are exposed as the type DssObjClass:
type DssObjClass = "containers" | "computers" | "ous" | "groups" | "users"
| Method | Endpoint | Returns |
|---|---|---|
dss.attributes() |
GET /attributes |
The schema for each object class. |
dss.domains() |
GET /domains |
Synced domains with status timestamps. |
dss.count(domain, objClass) |
GET /{objClass}/count?domain=... |
Entry count for that class. |
dss.query(objClass, filter?) |
POST /{objClass} |
Paginated entries matching filter. |
dss.getDssStats() |
(local) | Counters: queryCalls, domainCalls, attributeCalls, countCalls. |
DssQueryFilter supports name-based matching with operators
startWith, contain, equal against attributes
Common-Name, Distinguished Name, Name, SAM Account Name,
SID, User Principal Name, plus pagination via pageNumber and
pageSize (max 1000 per page).
4.5 Logging the SDK itself
Every operation logs through a single commonLogger instance:
import { setLogLevel, setLogger, LogLevel } from 'pancloud-nodejs'
setLogLevel(LogLevel.DEBUG) // Print everything
// Or replace the logger entirely:
setLogger({
level: LogLevel.INFO,
error: (e) => myMonitoring.report(e),
alert: (src, msg, name) => console.warn(`[${src.className}] ${msg}`),
info: (src, msg, name) => {},
debug: (src, msg, name, payload) => {}
})
LogLevel values: DEBUG = 0, INFO = 1, ALERT = 2, ERROR = 3. The
default is INFO.
Tip
Output lines are prefixed with
PANCLOUD:and contain a JSON object of{source, message, ...}. Pipe stdout throughjqor your log aggregator's JSON parser to slice them up.
5. Common Tasks
This section is a copy-paste cookbook for the eight workflows that show up in nearly every integration.
5.1 Run a one-shot logs query and print the records
Goal: retrieve traffic logs from the last hour, log how many you got, exit.
Prerequisites: PAN_CLIENT_ID, PAN_CLIENT_SECRET,
PAN_REFRESH_TOKEN, PAN_ENTRYPOINT set in environment.
Steps:
import { autoCredentials, LoggingService, EmitterInterface } from 'pancloud-nodejs'
async function main(): Promise<void> {
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
let totalRecords = 0
await ls.query({
query: "SELECT * FROM panw.traffic LIMIT 1000",
startTime: Math.floor(Date.now() / 1000) - 3600,
endTime: Math.floor(Date.now() / 1000),
logType: 'panw.traffic',
callBack: {
event: (e: EmitterInterface<any[]>) => {
if (e.message) {
totalRecords += e.message.length
console.log(`batch of ${e.message.length} (running total ${totalRecords})`)
}
}
}
})
console.log(`done, ${totalRecords} records`)
}
main().catch(e => console.error(e))
Verify: the script prints one or more batch of N lines, then
done, N records. The LoggingService stats reflect the same totals via
ls.getLsStats().
If it fails: start by raising the log level (setLogLevel(LogLevel.DEBUG)).
A 401 means the refresh token is bad — re-issue from the Cortex hub. A
PARSER error means the endpoint returned non-JSON, usually a transient
upstream issue; the SDK already retried 3 times.
5.2 Stream logs to a file as a Node Readable
Goal: pipe a query result straight to disk without buffering it in memory.
Steps:
import { createWriteStream } from 'fs'
import { autoCredentials, LoggingService, LoggingServiceClient } from 'pancloud-nodejs'
const cred = await autoCredentials()
const ls = LoggingService.factory(cred)
const stream = new LoggingServiceClient(ls, {
query: "SELECT * FROM panw.threat",
startTime: Math.floor(Date.now() / 1000) - 86400,
endTime: Math.floor(Date.now() / 1000)
})
const out = createWriteStream('threats.ndjson', { encoding: 'utf8' })
stream.on('data', (records: any[]) => {
for (const r of records) out.write(JSON.stringify(r) + '\n')
})
stream.on('end', () => out.end())
stream.on('error', e => { out.end(); console.error(e) })
Verify: wc -l threats.ndjson matches ls.getLsStats().records.
If it fails: LoggingServiceClient throws if the query stays in
RUNNING for more than retries polls (default 20, 200 ms apart =
4 seconds). Pass a higher retries for very large queries.
5.3 Subscribe to live traffic events and ack each batch
Goal: keep a long-running process consuming panw.traffic events,
acknowledging each batch so the channel does not back up.
Steps:
import { autoCredentials, EventService, EsFilterBuilderCfg } from 'pancloud-nodejs'
const cred = await autoCredentials()
const es = EventService.factory(cred)
const cfg: EsFilterBuilderCfg = {
filter: [{ table: "panw.traffic", timeout: 1000, batchSize: 8000 }],
filterOptions: {
callBack: {
event: e => console.log(`${e.message?.length ?? 0} events at ${new Date().toISOString()}`)
},
poolOptions: { ack: true, pollTimeout: 1000 }
}
}
await es.filterBuilder(cfg)
process.on('SIGINT', async () => {
es.pause()
await es.clearFilter(true)
process.exit(0)
})
Verify: the process prints a line every poll iteration. es.getEsStats()
shows polls climbing and acks matching it.
If it fails: if you see no output, the filter probably has no
matching traffic. Drop the WHERE clause and try panw.system, which
is always populated.
5.4 Page through Active Directory users
Goal: enumerate every user in a synced domain, 500 at a time.
Steps:
import { autoCredentials, DirectorySyncService } from 'pancloud-nodejs'
const cred = await autoCredentials()
const dss = await DirectorySyncService.factory(cred)
const domains = await dss.domains()
const dom = domains[0].dns
const total = await dss.count(dom, 'users')
console.log(`paging ${total} users in ${dom}`)
let page = 1
while (true) {
const resp = await dss.query('users', { domain: dom, pageNumber: page, pageSize: 500 })
if (resp.result.length === 0) break
console.log(`page ${page}: ${resp.result.length} entries (count=${resp.count})`)
if (resp.unreadResults === undefined || resp.unreadResults === 0) break
page++
}
Verify: the sum of result.length across pages equals the original
count.
If it fails: a 4xx response often means the directory hasn't been
synced yet — dss.domains()[0].status.statusUpdateTime will be 0.
5.5 Spin the SDK up for a quick test with a static token
Goal: validate connectivity in a 30-second test without provisioning a refresh-token flow.
Steps:
import { defaultCredentialsFactory, LoggingService } from 'pancloud-nodejs'
const cred = defaultCredentialsFactory(
'https://api.us.paloaltonetworks.com',
process.env.PAN_ACCESS_TOKEN!
)
const ls = LoggingService.factory(cred)
const job = await ls.query({
query: "SELECT * FROM panw.traffic LIMIT 1",
startTime: Math.floor(Date.now() / 1000) - 600,
endTime: Math.floor(Date.now() / 1000)
})
console.log(`status: ${job.queryStatus}, queryId: ${job.queryId}`)
Verify: queryStatus is one of RUNNING, FINISHED, JOB_FINISHED.
If it fails: if the SDK throws CONFIG: not a valid JWT access token, the value in PAN_ACCESS_TOKEN is missing the three
dot-separated segments. Re-fetch a fresh token.
5.6 Enable the L2/L3 correlation engine
Goal: receive synthesized correlation events that join L2 (DPI / MAC)
and L3 (traffic / threat) flows by sessionid.
Prerequisites: filter must include both DPI events for L2 and
panw.traffic or panw.threat for L3.
Steps:
import { autoCredentials, EventService, EsFilterBuilderCfg, L2correlation, EmitterInterface } from 'pancloud-nodejs'
const cred = await autoCredentials()
const es = EventService.factory(cred, {
l2Corr: { timeWindow: 120, absoluteTime: false, gcMultiplier: 0 }
})
const cfg: EsFilterBuilderCfg = {
filter: [
{ table: "panw.traffic", timeout: 1000 },
{ table: "panw.dpi", timeout: 1000 }
],
filterOptions: {
callBack: {
corr: (e: EmitterInterface<L2correlation[]>) => {
if (e.message) {
console.log(`${e.message.length} correlation events`)
e.message.forEach(x => console.log(` ${x.src} <-> ${x.dst} mac=${x["extended-traffic-log-mac"]}`))
}
}
},
poolOptions: { ack: true, pollTimeout: 1000 }
}
}
await es.filterBuilder(cfg)
setTimeout(async () => {
es.pause()
await es.clearFilter(true)
console.log(JSON.stringify(es.getEsStats(), undefined, ' '))
}, 120000)
Verify: es.getEsStats().correlationStats shows dbInserts > 0 and
dbWaterMark > 0.
If it fails: without panw.dpi events the engine has no L2 data and
will never produce correlations. Make sure the firewall sending data is
configured to forward extended-application logs.
5.7 Decode PCAP payloads from threat events
Goal: save every PCAP payload embedded in a panw.threat event to a
separate .pcap file.
Steps:
import { writeFileSync } from 'fs'
import { autoCredentials, EventService, EsFilterBuilderCfg, EmitterInterface } from 'pancloud-nodejs'
const cred = await autoCredentials()
const es = EventService.factory(cred)
let n = 0
const cfg: EsFilterBuilderCfg = {
filter: [{ table: "panw.threat", timeout: 1000 }],
filterOptions: {
callBack: {
pcap: (p: EmitterInterface<Buffer>) => {
if (p.message) {
const name = `pcap${('00' + n++).slice(-3)}.pcap`
writeFileSync(name, p.message)
console.log(`wrote ${name} (${p.message.length} bytes)`)
}
}
},
poolOptions: { ack: true, pollTimeout: 1000 }
},
flush: true
}
await es.filterBuilder(cfg)
Verify: files appear on disk; each is a valid libpcap file.
If it fails: Util.pcaptize() returns null if the event has no
pcap property, so no file is written. Switch to a higher-volume table
like panw.dpi to confirm the pipeline before debugging the threat
filter.
5.8 Turn on verbose tracing
Goal: see every HTTP request the SDK makes, with the JSON payload.
Steps:
import { setLogLevel, LogLevel, autoCredentials, EventService } from 'pancloud-nodejs'
setLogLevel(LogLevel.DEBUG)
const cred = await autoCredentials()
const es = EventService.factory(cred)
const f = await es.getFilters()
console.log(JSON.stringify(f, undefined, ' '))
Verify: stdout now shows PANCLOUD: ... debug lines for header
updates, fetches, and JSON payloads (truncated to 300 bytes).
If it fails: if you replaced the logger via setLogger(), your
custom debug implementation may be swallowing messages. Confirm by
reverting to the default commonLogger first.
6. Troubleshooting
Diagnostic decision tree
- Does
autoCredentials()succeed? If not, check env vars (PAN_CLIENT_IDetc.) andPAN_ENTRYPOINT. - Does the first HTTP call succeed? If you get
IDENTITYor 401, the refresh token is invalid or the entry point is wrong. - Is the query returning records? Confirm with
setLogLevel(LogLevel.DEBUG)and look at the JSON payload echoed in the logs.
Error: Environment variable PAN_CLIENT_ID not found or empty value
Cause: defaultCredentialsProviderFactory() (and therefore
autoCredentials() in memory mode) requires PAN_CLIENT_ID,
PAN_CLIENT_SECRET, PAN_REFRESH_TOKEN, and PAN_ENTRYPOINT to all be
present.
Solution:
printenv | grep PAN_to see what your shell has.- If you are inside Docker, confirm the variables made it through
docker run -e PAN_...orenv_file:. - As a fallback, pass the values directly:
defaultCredentialsProviderFactory({clientId, clientSecret, refreshToken, entryPoint}).
Prevention: validate all four variables at process start and exit non-zero with a clear message before constructing the SDK.
Error: not a valid JWT access token
Cause: defaultCredentialsFactory() and StaticCredentials decode
the JWT to extract its exp claim. The token must have three
dot-separated segments and a base64-encoded payload with a numeric exp.
Solution:
- Verify the token has three segments:
echo $TOKEN | tr '.' '\n' | wc -lshould print3. - Decode the middle segment:
echo "<segment>" | base64 -d | jq .and confirmexpis a number.
Prevention: do not log or copy access tokens through systems that strip or wrap whitespace; even a stray newline corrupts the JWT.
Error: PanCloudError() only "tms.traps" is accepted in the EventService
Cause: EventService.filterBuilder() rejects tms.analytics,
tms.config, tms.system, and tms.threat because the channel does
not deliver them.
Solution: drop those tables from your filter. If you need
tms.threat, query it through LoggingService instead.
Prevention: use the LogType TypeScript type for filter tables —
the compiler will not stop you from passing invalid values today, but
keeping the table list typed makes future SDK upgrades surface the
mismatch.
Error: Property 'accTokenGuardTime' must be, at max 3300 seconds
Cause: every credentials object refuses a guard time greater than 55 minutes because Cortex access tokens are typically 1-hour bearers.
Solution: set accTokenGuardTime (or the Credentials constructor's
guardTime) to a value below 3300. The default of 300 (5 minutes)
is appropriate for almost all deployments.
Prevention: if you find yourself wanting longer, you are likely attempting to keep tokens alive across a long blocking call. Refactor to release the credential object during long sleeps and let the next call re-fetch.
Error: Still in RUNNING state after 20 attempts
Cause: LoggingServiceClient polls a query 20 times at 200 ms
spacing. If the query is still computing, it gives up and deletes the
query.
Solution:
- For very large queries, raise
retriesanddelayin theLoggingServiceClientconstructor. - Switch to the auto-poll callback pattern; it has no built-in retry ceiling and runs as long as the query lives on the server.
Prevention: for queries that span months of data, narrow the
startTime/endTime window or add LIMIT to the SQL.
Error: HTTP 401 mid-stream
Cause: the access token expired and the refresh attempt failed (e.g. the refresh token was revoked).
Solution:
- Re-issue a refresh token from the Cortex hub.
- Restart the process so
autoCredentials()picks up the new value. - For long-running services, watch for repeated
IDENTITYerrors and surface them to your alerting layer; do not silently retry forever.
Prevention: rotate refresh tokens deliberately on a known cadence,
not by waiting for failures. Use fsCredentialsFactory() so the new
token persists across restarts without code changes.
Issue #30: Community Health Assessment
The PaloAltoNetworks/pancloud-nodejs repository tracks community-health items under issue #30. If you encounter behaviour that contradicts what this guide describes, check that issue (and the project's broader issue tracker) before filing a new ticket — the active maintainer cadence and contribution channels are documented there.
Caution
The repository's last source commit captured here is
e0d9e6bfrom 2019-10-29. The Cortex Data Lake API surface and OAuth2 flow may have changed since then. Always cross-check endpoint behaviour against the current Cortex Data Lake reference at docs.paloaltonetworks.com before debugging unexpected responses.
7. FAQ
Q: Why is the package called pancloud in package.json but I import it as pancloud-nodejs?
The package.json name field is pancloud, version 1.0.0. The
GitHub repository is pancloud-nodejs, and that is the npm install path.
The TypeScript test suite imports from 'pancloud-nodejs' and that is
the recommended import path for consumers.
Q: Do I need a separate @types/pancloud-nodejs package?
No. TypeScript declarations are emitted alongside the JavaScript by
build:library (the tsc -p tsconfig.json step). npm install brings
both.
Q: How does the SDK refresh access tokens?
Every Credentials subclass has a guardTime (default 300 seconds = 5
minutes). Before each HTTP request, the service client calls
Credentials.autoRefresh(), which checks whether the cached token
expires within guardTime. If yes, it calls retrieveAccessToken(),
which posts the refresh token to
https://api.paloaltonetworks.com/api/oauth2/RequestToken and stores
the new access token. Static credentials skip this — they are no-ops.
Q: What is the difference between LoggingService and EventService?
LoggingService runs ad-hoc SQL queries against historical log indexes
and returns a finite result set in batches. EventService opens a
long-lived channel that emits new events as they happen, with optional
filters. Use LoggingService for "show me everything between Tuesday
and Thursday"; use EventService for "stream me new threat events
forever".
Q: Can I run multiple service clients off the same Credentials?
Yes. The Credentials object is the auth-token cache. You can pass it
into LoggingService.factory(), EventService.factory(), and
DirectorySyncService.factory() simultaneously. Each client manages its
own per-instance state (channel ID, query queue, stats).
Q: How do I cancel an in-flight query?
Call ls.cancelPoll(queryId) to remove it from the auto-poll queue and
issue a server-side delete, or call ls.deleteQuery(queryId) directly
for the same effect on the server side without touching the local queue.
Q: What does flush: true do in an Event Service filter?
When you set a filter with flush: true, the channel discards every
event currently buffered server-side. Use it when you have changed the
filter so significantly that old events are no longer relevant. The
default is flush: false (keep buffered events).
Q: Can I use this SDK in a browser?
No. The SDK depends on Node's https, crypto, fs, and events
modules. There is no browser bundle and no plan to ship one — Cortex
Data Lake credentials should not live in a browser anyway.
Q: Where is the published TypeScript source?
The lib/ directory at the repository root contains the canonical
TypeScript. tsc compiles it into dist/, which is what package.json
points main at. There is no separate src/ versus dist/ split in
the repo at commit e0d9e6b.
Q: Does the SDK validate JSON responses?
Yes — every response goes through a hand-written type guard
(isJobResult, isEsFilter, isDssDomain, etc.) before it is exposed
to your code. A response that does not match the expected schema raises
PanCloudError(PARSER, ...).
Q: Is tms.traps the only tms.* table I can subscribe to?
For Event Service, yes. EventService.filterBuilder() validates against
the invalidTables list (tms.analytics, tms.config, tms.system,
tms.threat). All panw.* tables are accepted. LoggingService has
no such restriction — you can query any indexed table.
Q: How do I test code that uses this SDK without a real Cortex tenant?
Use defaultCredentialsFactory(entryPoint, fakeJwt) to build a static
credentials object, then mock the underlying fetch module. The SDK has
a single internal fetch export (lib/fetch.ts) that wraps Node's
https.request; intercepting it gives you full control over responses.
Q: How do I support multiple Cortex Data Lake tenants in one process?
Use fsCredentialsFactory() (or implement your own subclass of
CortexCredentialProvider) and call registerManualDatalake() once per
tenant. Each call returns its own Credentials object bound to its own
datalake ID. The provider stores all of them in a single encrypted
PANCLOUD_CONFIG.json and refreshes each independently.
Q: What is the L2/L3 correlation engine?
It is an optional in-memory join that pairs L3 (IP) flows with L2 (MAC)
flows by sessionid. When you pass l2Corr: { timeWindow, absoluteTime, gcMultiplier } to EventService.factory(), the SDK buffers L2 and L3
events and emits a synthesized "correlated" event whenever a session
appears in both — see Section 5.6 for the full pattern.