From b2ef279571ed6a1753522d5fe5a3d34c74ff16dd Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 14:28:10 -0400 Subject: [PATCH 1/6] Add Zscaler Trust Status plugin Add a new Zscaler Trust Status plugin that reads the public Trust RSS feed, tracks incidents, maintenance, and advisories, and rolls them up into cloud health summaries. This includes configuration validation, feed parsing scripts, default dashboards (Overview, Incidents, Maintenance & Advisories), and setup documentation for the FedRAMP and commercial Trust portals. --- plugins/Zscaler/v1/configValidation.json | 13 + .../Zscaler/v1/dataStreams/cloudStatus.json | 103 +++ .../v1/dataStreams/feedValidation.json | 27 + plugins/Zscaler/v1/dataStreams/posts.json | 299 ++++++ .../v1/dataStreams/scripts/cloudStatus.js | 169 ++++ .../v1/dataStreams/scripts/feedValidation.js | 20 + .../Zscaler/v1/dataStreams/scripts/posts.js | 231 +++++ .../v1/defaultContent/incidents.dash.json | 859 ++++++++++++++++++ .../maintenanceAndAdvisories.dash.json | 708 +++++++++++++++ .../Zscaler/v1/defaultContent/manifest.json | 16 + .../v1/defaultContent/overview.dash.json | 786 ++++++++++++++++ plugins/Zscaler/v1/docs/README.md | 91 ++ plugins/Zscaler/v1/icon.svg | 4 + plugins/Zscaler/v1/metadata.json | 36 + plugins/Zscaler/v1/ui.json | 36 + 15 files changed, 3398 insertions(+) create mode 100644 plugins/Zscaler/v1/configValidation.json create mode 100644 plugins/Zscaler/v1/dataStreams/cloudStatus.json create mode 100644 plugins/Zscaler/v1/dataStreams/feedValidation.json create mode 100644 plugins/Zscaler/v1/dataStreams/posts.json create mode 100644 plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js create mode 100644 plugins/Zscaler/v1/dataStreams/scripts/feedValidation.js create mode 100644 plugins/Zscaler/v1/dataStreams/scripts/posts.js create mode 100644 plugins/Zscaler/v1/defaultContent/incidents.dash.json create mode 100644 plugins/Zscaler/v1/defaultContent/maintenanceAndAdvisories.dash.json create mode 100644 plugins/Zscaler/v1/defaultContent/manifest.json create mode 100644 plugins/Zscaler/v1/defaultContent/overview.dash.json create mode 100644 plugins/Zscaler/v1/docs/README.md create mode 100644 plugins/Zscaler/v1/icon.svg create mode 100644 plugins/Zscaler/v1/metadata.json create mode 100644 plugins/Zscaler/v1/ui.json diff --git a/plugins/Zscaler/v1/configValidation.json b/plugins/Zscaler/v1/configValidation.json new file mode 100644 index 00000000..b75f81fa --- /dev/null +++ b/plugins/Zscaler/v1/configValidation.json @@ -0,0 +1,13 @@ +{ + "steps": [ + { + "displayName": "Read the Trust portal feed", + "dataStream": { + "name": "feedValidation" + }, + "required": true, + "error": "Could not read the Zscaler Trust RSS feed. Check the selected Trust portal is reachable from the SquaredUp agent or cloud.", + "success": "Feed read successfully." + } + ] +} diff --git a/plugins/Zscaler/v1/dataStreams/cloudStatus.json b/plugins/Zscaler/v1/dataStreams/cloudStatus.json new file mode 100644 index 00000000..887188b9 --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/cloudStatus.json @@ -0,0 +1,103 @@ +{ + "name": "cloudStatus", + "displayName": "Cloud Status", + "description": "Current status of each Zscaler cloud, rolled up from active incidents and maintenance on the Trust portal", + "tags": [ + "Status", + "Incidents" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "rss-feed", + "paging": { + "mode": "none" + }, + "postRequestScript": "cloudStatus.js" + }, + "matches": "none", + "metadata": [ + { + "name": "cloud", + "displayName": "Cloud", + "shape": "string", + "role": "label" + }, + { + "name": "product", + "displayName": "Product", + "shape": "string" + }, + { + "name": "fedrampLevel", + "displayName": "FedRAMP Level", + "shape": "string" + }, + { + "name": "severity", + "displayName": "Severity", + "shape": "number", + "visible": false + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "statusText", + "displayName": "Status", + "shape": "string", + "role": "description" + }, + { + "name": "activeIncidents", + "displayName": "Active Incidents", + "shape": ["number", { "decimalPlaces": 0 }] + }, + { + "name": "activeMaintenance", + "displayName": "Active Maintenance", + "shape": ["number", { "decimalPlaces": 0 }] + }, + { + "name": "upcomingMaintenance", + "displayName": "Upcoming Maintenance", + "shape": ["number", { "decimalPlaces": 0 }] + }, + { + "name": "activeAdvisories", + "displayName": "Active Advisories", + "shape": ["number", { "decimalPlaces": 0 }] + }, + { + "name": "incidentsLast30Days", + "displayName": "Incidents (Last 30 Days)", + "shape": ["number", { "decimalPlaces": 0 }] + }, + { + "name": "lastIncident", + "displayName": "Last Incident", + "shape": "date" + }, + { + "name": "nextMaintenance", + "displayName": "Next Maintenance", + "shape": "date" + }, + { + "name": "currentEventLink", + "displayName": "Current Event", + "shape": ["url", { "label": "View post" }] + } + ], + "defaultShaping": { + "sort": { + "by": [ + ["severity", "desc"], + ["cloud", "asc"] + ] + } + }, + "timeframes": false +} diff --git a/plugins/Zscaler/v1/dataStreams/feedValidation.json b/plugins/Zscaler/v1/dataStreams/feedValidation.json new file mode 100644 index 00000000..ec25b8d6 --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/feedValidation.json @@ -0,0 +1,27 @@ +{ + "name": "feedValidation", + "displayName": "Feed Validation", + "description": "Confirms the selected Trust portal returns a parseable RSS document", + "tags": [ + "Status" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "rss-feed", + "paging": { + "mode": "none" + }, + "postRequestScript": "feedValidation.js" + }, + "matches": "none", + "visibility": { + "type": "hidden" + }, + "metadata": [ + { + "pattern": ".*" + } + ], + "timeframes": false +} diff --git a/plugins/Zscaler/v1/dataStreams/posts.json b/plugins/Zscaler/v1/dataStreams/posts.json new file mode 100644 index 00000000..67bb18c2 --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/posts.json @@ -0,0 +1,299 @@ +{ + "name": "posts", + "displayName": "Posts", + "description": "Incidents, scheduled maintenance and advisories from the Zscaler Trust portal, one row per post", + "tags": [ + "Status", + "Incidents", + "Maintenance" + ], + "baseDataSourceName": "httpRequestUnscoped", + "config": { + "httpMethod": "get", + "endpointPath": "rss-feed", + "paging": { + "mode": "none" + }, + "postRequestScript": "posts.js" + }, + "matches": "none", + "ui": [ + { + "type": "choiceChips", + "name": "type", + "label": "Type", + "isMulti": true, + "help": "Leave empty to include every type of post", + "options": [ + { + "value": "Incident", + "label": "Incident" + }, + { + "value": "Maintenance", + "label": "Maintenance" + }, + { + "value": "Advisory", + "label": "Advisory" + } + ] + }, + { + "type": "autocomplete", + "name": "cloud", + "label": "Cloud(s)", + "isMulti": true, + "isClearable": true, + "allowCustomValues": true, + "help": "Only posts affecting these clouds. Type a value to use a cloud that isn't listed. Leave empty to include every cloud.", + "data": { + "source": "fixed", + "values": [ + { + "value": "zscalergov.net", + "label": "zscalergov.net" + }, + { + "value": "zscalerten.net", + "label": "zscalerten.net" + }, + { + "value": "zpagov.net", + "label": "zpagov.net" + }, + { + "value": "zpagov.us", + "label": "zpagov.us" + }, + { + "value": "zdxgov.net", + "label": "zdxgov.net" + }, + { + "value": "zdxten.net", + "label": "zdxten.net" + }, + { + "value": "zidentitygov.net", + "label": "zidentitygov.net" + }, + { + "value": "zidentitygov.us", + "label": "zidentitygov.us" + }, + { + "value": "zscaler.net", + "label": "zscaler.net" + }, + { + "value": "zscalerone.net", + "label": "zscalerone.net" + }, + { + "value": "zscalertwo.net", + "label": "zscalertwo.net" + }, + { + "value": "zscalerthree.net", + "label": "zscalerthree.net" + }, + { + "value": "zscloud.net", + "label": "zscloud.net" + }, + { + "value": "private.zscaler.com", + "label": "private.zscaler.com" + }, + { + "value": "zpatwo.net", + "label": "zpatwo.net" + } + ] + } + }, + { + "type": "toggle", + "name": "oneRowPerCloud", + "label": "One row per affected cloud", + "defaultValue": false, + "help": "Posts that affect several clouds return one row for each cloud, so tiles can group by cloud. When off, each post is one row and Cloud shows the first affected cloud." + }, + { + "type": "switch", + "name": "dateField", + "label": "Timeframe applies to", + "defaultValue": "published", + "help": "Which date the selected timeframe filters on. Set the timeframe to **None** to return everything in the feed (about 3 months).", + "options": [ + { + "value": "published", + "label": "Published" + }, + { + "value": "eventWindow", + "label": "Event window" + } + ], + "tileEditorStep": [ + "Timeframe" + ] + } + ], + "metadata": [ + { + "name": "id", + "displayName": "ID", + "shape": "string", + "role": "id", + "visible": false + }, + { + "name": "title", + "displayName": "Title", + "shape": "string", + "role": "label" + }, + { + "name": "type", + "displayName": "Type", + "shape": "string" + }, + { + "name": "eventType", + "displayName": "Event type", + "shape": "string" + }, + { + "name": "status", + "displayName": "Status", + "shape": "string" + }, + { + "name": "phase", + "displayName": "Phase", + "shape": "string" + }, + { + "name": "state", + "displayName": "State", + "shape": "state" + }, + { + "name": "cloud", + "displayName": "Cloud", + "shape": "string" + }, + { + "name": "clouds", + "displayName": "Clouds", + "shape": "string" + }, + { + "name": "product", + "displayName": "Product", + "shape": "string" + }, + { + "name": "fedrampLevel", + "displayName": "FedRAMP level", + "shape": "string" + }, + { + "name": "published", + "displayName": "Published", + "shape": "date", + "role": "timestamp" + }, + { + "name": "startTime", + "displayName": "Start time", + "shape": "date" + }, + { + "name": "endTime", + "displayName": "End time", + "shape": "date" + }, + { + "name": "resolvedTime", + "displayName": "Resolved time", + "shape": "date" + }, + { + "name": "lastUpdated", + "displayName": "Last updated", + "shape": "date" + }, + { + "name": "durationMinutes", + "displayName": "Duration", + "shape": "minutes" + }, + { + "name": "timeToResolveMinutes", + "displayName": "Time to resolve", + "shape": "minutes" + }, + { + "name": "howFound", + "displayName": "How found", + "shape": "string" + }, + { + "name": "nextUpdate", + "displayName": "Next update", + "shape": "string" + }, + { + "name": "customerImpact", + "displayName": "Customer impact", + "shape": "string" + }, + { + "name": "workaround", + "displayName": "Workaround", + "shape": "string" + }, + { + "name": "summary", + "displayName": "Summary", + "shape": "string" + }, + { + "name": "updateCount", + "displayName": "Updates", + "shape": "number" + }, + { + "name": "latestUpdate", + "displayName": "Latest update", + "shape": "string" + }, + { + "name": "link", + "displayName": "Link", + "shape": [ + "url", + { + "label": "View post" + } + ], + "role": "link" + } + ], + "defaultShaping": { + "sort": { + "by": [ + [ + "published", + "desc" + ] + ] + } + }, + "timeframes": true, + "supportsNoneTimeframe": true, + "defaultTimeframe": "none" +} diff --git a/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js b/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js new file mode 100644 index 00000000..d0e46f56 --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js @@ -0,0 +1,169 @@ +// Parsing rules below are shared with posts.js - keep the two in step. +const channel = data && data.rss && data.rss.channel ? data.rss.channel[0] : undefined; + +if (!channel) { + api.report.error('The Trust portal did not return an RSS document.'); +} + +// The XML handler wraps every child element in an array, and elements with attributes +// (category, guid) become objects whose text lives in `_`. +const text = (node) => { + let v = Array.isArray(node) ? node[0] : node; + if (v && typeof v === 'object') { + v = v._; + } + return typeof v === 'string' ? v.trim() : undefined; +}; +const toDate = (s) => { + const d = s ? new Date(s) : undefined; + return d && !isNaN(d.getTime()) ? d : undefined; +}; +const productOf = (cloud) => { + if (/^zpa|^private\.zscaler/.test(cloud)) return 'ZPA'; + if (/^zdx/.test(cloud)) return 'ZDX'; + if (/^zidentity/.test(cloud)) return 'ZIdentity'; + if (/^ztb-/.test(cloud)) return 'Zero Trust Branch'; + if (/^zscaler|^zscloud/.test(cloud)) return 'ZIA'; + return 'Other'; +}; +const TYPES = { 'Recent incident': 'Incident', 'Scheduled maintenance': 'Maintenance', Advisory: 'Advisory' }; + +const now = new Date(); +const thirtyDaysAgo = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + +const posts = []; +for (const item of channel.item || []) { + const type = TYPES[text(item.category)]; + if (!type) continue; + + const title = text(item.title) || ''; + const link = text(item.link); + const sep = title.lastIndexOf(' - '); + const suffix = sep >= 0 ? title.slice(sep + 3) : ''; + let headline = sep >= 0 ? title.slice(0, sep).trim() : title; + + const clouds = []; + for (const entry of suffix.split(',')) { + const m = entry.trim().match(/^([^(]*?)\s*(?:\((.*)\))?$/); + const cloud = m ? m[1].trim().toLowerCase() : ''; + if (/^[a-z0-9.-]+$/.test(cloud) && (cloud.includes('.') || /^ztb-/.test(cloud))) { + const level = m[2] ? m[2].replace(/^FedRAMP\s*/i, '').trim() : undefined; + clouds.push({ cloud, level: level || undefined }); + } + } + if (clouds.length === 0) { + const segment = link ? link.replace(/^https?:\/\//, '').split('/')[1] : undefined; + if (segment) { + clouds.push({ cloud: segment.toLowerCase(), level: undefined }); + } + headline = title; + } + + const status = text(item.Status); + const start = toDate(text(item.startTime)); + const end = toDate(text(item.endTime)); + const resolved = text(item.ResolvedDate); + const pub = toDate(text(item.pubDate)); + + let phase; + if (type === 'Incident') { + phase = ['Resolved', 'Remediated', 'Cancelled'].includes(status) || resolved ? 'Closed' : 'Active'; + } else if (type === 'Maintenance') { + if (status === 'Upcoming') phase = 'Upcoming'; + else if (status === 'In Progress') phase = 'Active'; + else if (status === 'Completed' || status === 'Cancelled') phase = 'Closed'; + else if (start && start > now) phase = 'Upcoming'; + else if (end && end > now) phase = 'Active'; + else phase = 'Closed'; + } else { + phase = resolved ? 'Closed' : 'Active'; + } + + posts.push({ headline, link, clouds, type, status, eventType: text(item.eventType), phase, start, when: start || pub }); +} + +const configured = (context.dataSources && context.dataSources[0] && context.dataSources[0].clouds) || ''; +const wanted = String(configured) + .split(',') + .map((c) => c.trim().toLowerCase()) + .filter(Boolean); + +const byCloud = new Map(); +const rowFor = (cloud) => { + if (!byCloud.has(cloud)) { + byCloud.set(cloud, { cloud, level: undefined, posts: [] }); + } + return byCloud.get(cloud); +}; +// A configured cloud with no posts still gets a row so it shows as operational. +for (const cloud of wanted) { + rowFor(cloud); +} +for (const post of posts) { + for (const c of post.clouds) { + if (wanted.length > 0 && !wanted.includes(c.cloud)) continue; + const row = rowFor(c.cloud); + row.level = row.level || c.level; + row.posts.push(post); + } +} + +const latest = (list) => list.slice().sort((a, b) => (b.when || 0) - (a.when || 0))[0]; +const iso = (d) => (d ? d.toISOString() : undefined); + +result = Array.from(byCloud.values()).map((row) => { + const of = (type, phase) => row.posts.filter((p) => p.type === type && p.phase === phase); + const activeIncidents = of('Incident', 'Active'); + const activeMaintenance = of('Maintenance', 'Active'); + const upcoming = of('Maintenance', 'Upcoming'); + const advisories = of('Advisory', 'Active'); + const incidents = row.posts.filter((p) => p.type === 'Incident'); + + // Only an outage turns a cloud red - degradations (often cosmetic, and sometimes left + // "In Progress" on the portal for weeks) and incidents under monitoring are a warning. + const isOutage = (p) => p.eventType === 'Service Disruption' && p.status !== 'Monitoring'; + const major = activeIncidents.filter(isOutage); + const minor = activeIncidents + .filter((p) => !isOutage(p)) + .map((p) => ({ post: p, label: p.status === 'Monitoring' ? 'Monitoring' : 'Incident' })) + .concat(activeMaintenance.map((p) => ({ post: p, label: 'Maintenance in progress' }))); + + let severity = 0; + let state = 'success'; + let driver; + let label; + if (major.length > 0) { + severity = 2; + state = 'error'; + driver = latest(major); + label = 'Incident'; + } else if (minor.length > 0) { + severity = 1; + state = 'warning'; + driver = latest(minor.map((m) => m.post)); + label = minor.find((m) => m.post === driver).label; + } + + const nextMaintenance = upcoming + .map((p) => p.start) + .filter(Boolean) + .sort((a, b) => a - b)[0]; + const lastIncident = latest(incidents.filter((p) => p.when)); + + return { + cloud: row.cloud, + product: productOf(row.cloud), + fedrampLevel: row.level, + severity, + state, + statusText: driver ? `${label}: ${driver.headline}` : 'Operational', + activeIncidents: activeIncidents.length, + activeMaintenance: activeMaintenance.length, + upcomingMaintenance: upcoming.length, + activeAdvisories: advisories.length, + incidentsLast30Days: incidents.filter((p) => p.when && p.when >= thirtyDaysAgo).length, + lastIncident: iso(lastIncident && lastIncident.when), + nextMaintenance: iso(nextMaintenance), + currentEventLink: driver ? driver.link : undefined + }; +}); diff --git a/plugins/Zscaler/v1/dataStreams/scripts/feedValidation.js b/plugins/Zscaler/v1/dataStreams/scripts/feedValidation.js new file mode 100644 index 00000000..bcaec574 --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/scripts/feedValidation.js @@ -0,0 +1,20 @@ +// A 2xx alone doesn't prove the feed is usable - an HTML error or maintenance page would also +// arrive as 200 and reach this script as `data === undefined`. Check it actually parsed as RSS. +const channel = data && data.rss && data.rss.channel ? data.rss.channel[0] : undefined; + +if (!channel) { + api.report.error('The Trust portal did not return an RSS document.'); +} + +const items = channel.item || []; + +if (items.length === 0) { + api.report.error('The Trust portal feed returned RSS but contained no posts.'); +} + +result = [ + { + feedTitle: channel.title ? channel.title[0] : undefined, + posts: items.length + } +]; diff --git a/plugins/Zscaler/v1/dataStreams/scripts/posts.js b/plugins/Zscaler/v1/dataStreams/scripts/posts.js new file mode 100644 index 00000000..90e7d11b --- /dev/null +++ b/plugins/Zscaler/v1/dataStreams/scripts/posts.js @@ -0,0 +1,231 @@ +// Parsing rules below are shared with cloudStatus.js - keep the two in step. +const channel = data && data.rss && data.rss.channel ? data.rss.channel[0] : undefined; + +if (!channel) { + api.report.error('The Trust portal did not return an RSS document.'); +} + +// The XML parser wraps every child element in an array, and elements that carry attributes +// (category, guid) become objects with the text under `_`. +const text = (el) => { + const v = Array.isArray(el) ? el[0] : el; + const s = v && typeof v === 'object' ? v._ : v; + return s === undefined || s === null || String(s).trim() === '' ? undefined : String(s).trim(); +}; + +const toDate = (s) => { + if (!s) return undefined; + const d = new Date(s); + return isNaN(d.getTime()) ? undefined : d; +}; + +const iso = (d) => (d ? d.toISOString() : undefined); + +const stripHtml = (html) => { + if (!html) return undefined; + const s = String(html) + .replace(/<(br|\/p|\/li|\/div)[^>]*>/gi, ' ') + .replace(/<[^>]+>/g, '') + .replace(/ /g, ' ') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/�?39;/g, "'") + .replace(/&/g, '&') + .replace(/\s+/g, ' ') + .trim(); + return s || undefined; +}; + +const truncate = (s, max) => (s && s.length > max ? s.slice(0, max - 1).trimEnd() + '…' : s); + +const minutesBetween = (from, to) => { + if (!from || !to) return undefined; + const m = Math.round((to - from) / 60000); + return m >= 0 ? m : undefined; +}; + +// Filter values arrive as plain strings (--ui) or as { value } objects (autocomplete in the tile editor). +const listParam = (v) => + (Array.isArray(v) ? v : v ? [v] : []) + .map((x) => (x && typeof x === 'object' ? x.value : x)) + .map((x) => String(x || '').trim().toLowerCase()) + .filter(Boolean); + +const productOf = (cloud) => { + if (/^zpa|^private\.zscaler/.test(cloud)) return 'ZPA'; + if (/^zdx/.test(cloud)) return 'ZDX'; + if (/^zidentity/.test(cloud)) return 'ZIdentity'; + if (/^ztb-/.test(cloud)) return 'Zero Trust Branch'; + if (/^zscaler|^zscloud/.test(cloud)) return 'ZIA'; + return 'Other'; +}; + +const TYPES = { 'Recent incident': 'Incident', 'Scheduled maintenance': 'Maintenance', Advisory: 'Advisory' }; + +// Titles end with " - (), ()", but the headline can contain " - " too, +// so only the text after the LAST one is treated as the cloud list. +const parseTitle = (rawTitle, link) => { + const title = (rawTitle || '').trim(); + const cut = title.lastIndexOf(' - '); + const clouds = []; + + if (cut >= 0) { + for (const entry of title.slice(cut + 3).split(',')) { + const m = entry.trim().match(/^([^(]*?)\s*(?:\(([^)]*)\))?$/); + const cloud = m ? m[1].trim().toLowerCase() : ''; + if (/^[a-z0-9.-]+$/.test(cloud) && (cloud.includes('.') || /^ztb-/.test(cloud))) { + clouds.push({ cloud, level: m[2] ? m[2].replace(/^FedRAMP\s+/i, '').trim() : undefined }); + } + } + } + + if (clouds.length > 0) { + return { headline: title.slice(0, cut).trim(), clouds }; + } + + const segment = (link || '').replace(/^https?:\/\/[^/]+\//, '').split('/')[0].toLowerCase(); + return { headline: title, clouds: segment ? [{ cloud: segment, level: undefined }] : [] }; +}; + +const parseDescription = (html) => { + const raw = html || ''; + const cut = raw.search(/]*class="post-updates"/); + const body = cut >= 0 ? raw.slice(0, cut) : raw; + const updates = []; + + if (cut >= 0) { + for (const chunk of raw.slice(cut).split('

').slice(1)) { + const time = chunk.match(/class="report-time">([^<]*)') + 4); + updates.push({ time: toDate(time ? time[1].trim() : undefined), text: stripHtml(afterHeader) }); + } + } + + const para = body.match(/]*class="post_body"[^>]*>([\s\S]*?)<\/p>/) || body.match(/]*>([\s\S]*?)<\/p>/); + return { firstParagraph: stripHtml(para ? para[1] : body), updates }; +}; + +const now = new Date(); +const config = context.config || {}; +const dataSource = (context.dataSources && context.dataSources[0]) || {}; + +const sourceClouds = listParam((dataSource.clouds || '').split(',')); +const wantedTypes = listParam(config.type); +const wantedClouds = listParam(config.cloud); +const oneRowPerCloud = config.oneRowPerCloud === true || config.oneRowPerCloud === 'true'; +const dateField = config.dateField === 'eventWindow' ? 'eventWindow' : 'published'; + +// A tile set to None still receives a (24 hour) start/end - the enum is the only reliable signal. +const tf = context.timeframe; +const windowStart = tf && tf.enum !== 'none' ? toDate(tf.start) : undefined; +const windowEnd = tf && tf.enum !== 'none' ? toDate(tf.end) : undefined; + +const rows = []; + +for (const item of channel.item || []) { + const link = text(item.link); + const { headline, clouds } = parseTitle(text(item.title), link); + const cloudNames = clouds.map((c) => c.cloud); + + if (sourceClouds.length > 0 && !cloudNames.some((c) => sourceClouds.includes(c))) continue; + + const type = TYPES[text(item.category)] || text(item.category); + if (wantedTypes.length > 0 && !wantedTypes.includes(String(type).toLowerCase())) continue; + if (wantedClouds.length > 0 && !cloudNames.some((c) => wantedClouds.includes(c))) continue; + + const published = toDate(text(item.pubDate)); + const startTime = toDate(text(item.startTime)); + const endTime = toDate(text(item.endTime)); + const resolved = toDate(text(item.ResolvedDate)); + const status = text(item.Status); + + let phase; + if (type === 'Maintenance') { + if (status === 'Upcoming') phase = 'Upcoming'; + else if (status === 'In Progress') phase = 'Active'; + else if (status === 'Completed' || status === 'Cancelled') phase = 'Closed'; + else if (startTime && startTime > now) phase = 'Upcoming'; + else if (endTime && endTime > now) phase = 'Active'; + else phase = 'Closed'; + } else if (type === 'Incident') { + phase = ['Resolved', 'Remediated', 'Cancelled'].includes(status) || resolved ? 'Closed' : 'Active'; + } else { + phase = resolved ? 'Closed' : 'Active'; + } + + let state = 'success'; + if (phase === 'Active') { + // Matches cloudStatus.js: only an outage is an error; degradations and monitoring are warnings. + const isOutage = text(item.eventType) === 'Service Disruption' && status !== 'Monitoring'; + if (type === 'Incident') state = isOutage ? 'error' : 'warning'; + else if (type === 'Maintenance') state = 'warning'; + else state = 'unknown'; + } else if (phase === 'Upcoming') { + state = 'unknown'; + } + + if (windowStart && windowEnd) { + if (dateField === 'published') { + if (!published || published < windowStart || published > windowEnd) continue; + } else { + const from = startTime || published; + const to = endTime || resolved || (phase === 'Closed' ? from : now); + if (!from || from > windowEnd || to < windowStart) continue; + } + } + + const { firstParagraph, updates } = parseDescription(text(item.description)); + const dated = updates.filter((u) => u.time); + const newest = dated.length > 0 ? dated.reduce((a, b) => (b.time > a.time ? b : a)) : undefined; + const latest = newest || updates[0]; + const guid = text(item.guid); + const levels = [...new Set(clouds.map((c) => c.level).filter(Boolean))]; + + const base = { + id: guid ? guid.split(' ')[0] : (link || '').split('/').pop(), + title: headline, + type, + eventType: text(item.eventType), + status, + phase, + state, + clouds: cloudNames.join(', '), + published: iso(published), + startTime: iso(startTime), + endTime: iso(endTime), + resolvedTime: iso(resolved), + lastUpdated: iso(newest ? newest.time : published), + durationMinutes: minutesBetween(startTime, endTime), + timeToResolveMinutes: type === 'Incident' ? minutesBetween(startTime, resolved) : undefined, + howFound: text(item.HowFound), + nextUpdate: text(item.nextUpdate), + customerImpact: stripHtml(text(item.customerImpact)), + workaround: stripHtml(text(item.availableWorkaround)), + summary: truncate(stripHtml(text(item.summary)) || firstParagraph, 400), + updateCount: updates.length, + latestUpdate: truncate(latest ? latest.text : undefined, 400), + link + }; + + if (oneRowPerCloud) { + for (const c of clouds) { + rows.push({ + ...base, + id: `${base.id}-${c.cloud}`, + cloud: c.cloud, + product: productOf(c.cloud), + fedrampLevel: c.level + }); + } + } else { + rows.push({ + ...base, + cloud: cloudNames[0], + product: cloudNames.length > 0 ? productOf(cloudNames[0]) : 'Other', + fedrampLevel: levels.length > 0 ? levels.join(', ') : undefined + }); + } +} + +result = rows; diff --git a/plugins/Zscaler/v1/defaultContent/incidents.dash.json b/plugins/Zscaler/v1/defaultContent/incidents.dash.json new file mode 100644 index 00000000..e14cb2a1 --- /dev/null +++ b/plugins/Zscaler/v1/defaultContent/incidents.dash.json @@ -0,0 +1,859 @@ +{ + "name": "Incidents", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "0767827e-202a-4807-93f3-089b9771b335", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents", + "description": "Incidents published in the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Incidents" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Incidents" + } + } + } + } + }, + { + "i": "589c64cc-ff28-4a51-8619-286156eb89cf", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Outages", + "description": "Incidents classed as a service disruption", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "eventType", + "operation": "equals", + "value": "Service Disruption" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Outages" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Outages" + } + } + } + } + }, + { + "i": "c8ca3605-6453-44c1-9448-d4b4cad41139", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Mean Time To Resolve", + "description": "Average time from start to resolution of resolved incidents", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "mean", + "names": [ + "timeToResolveMinutes" + ] + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "timeToResolveMinutes_mean", + "type": "quantitative", + "title": "Mean Time To Resolve" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "timeToResolveMinutes_mean", + "comparisonColumn": "none", + "label": "Mean Time To Resolve" + } + } + } + } + }, + { + "i": "a47aa8d7-6f38-4eca-b44c-01afb8c70eda", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Customer Reported", + "description": "Incidents first reported by customers", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "howFound", + "operation": "equals", + "value": "Customer Reported" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Customer Reported" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Customer Reported" + } + } + } + } + }, + { + "i": "58af9e70-a6d1-4cf8-a2d7-33b658217357", + "x": 0, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents Over Time", + "description": "Incidents published per day by event type", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "group": { + "by": [ + [ + "published", + "byDay" + ], + [ + "eventType", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "published_byDay", + "asc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "published_byDay", + "type": "temporal", + "title": "Published" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Incidents" + }, + "series": { + "field": "eventType_uniqueValues", + "type": "nominal", + "title": "Event Type" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom" + } + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "published_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "eventType_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "1761edb0-47d6-4f59-8c86-5be3440da1e5", + "x": 2, + "y": 2, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "How Incidents Were Found", + "description": "Whether incidents were detected internally or reported by customers", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "howFound", + "operation": "notempty" + } + ] + }, + "group": { + "by": [ + [ + "howFound", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "pie", + "encoding": { + "label": { + "field": "howFound_uniqueValues", + "type": "nominal", + "title": "How Found" + }, + "value": { + "field": "count", + "type": "quantitative", + "title": "Incidents" + } + }, + "style": { + "pieInnerRadius": 60, + "pieDonutCenterValue": "total", + "labelFontSize": 16 + } + } + }, + "visualisation": { + "type": "data-stream-donut-chart", + "config": { + "data-stream-donut-chart": { + "valueColumn": "count", + "labelColumn": "howFound_uniqueValues", + "hideCenterValue": false, + "showValuesAsPercentage": true, + "legendPosition": "auto", + "legendMode": "table" + } + } + } + } + }, + { + "i": "bf8be6c6-ff77-4b81-ac48-b9e4c5373485", + "x": 0, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents By Cloud", + "description": "Incidents affecting each cloud (an incident on several clouds counts against each)", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ], + "oneRowPerCloud": true + }, + "group": { + "by": [ + [ + "cloud", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "cloud_uniqueValues", + "type": "nominal", + "title": "Cloud" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Incidents" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true + } + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "cloud_uniqueValues", + "yAxisData": [ + "count" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "14f580be-74fe-4f9a-964f-fb1efa1899a7", + "x": 2, + "y": 5, + "w": 2, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Time To Resolve", + "description": "The 15 longest-running resolved incidents", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "filter": { + "multiOperation": "and", + "filters": [ + { + "column": "timeToResolveMinutes", + "operation": "notempty" + } + ] + }, + "sort": { + "by": [ + [ + "timeToResolveMinutes", + "desc" + ] + ], + "top": 15 + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "title", + "type": "nominal", + "title": "Title" + }, + "y": { + "field": "timeToResolveMinutes", + "type": "quantitative", + "title": "Time To Resolve (min)" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true + } + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "title", + "yAxisData": [ + "timeToResolveMinutes" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "55f302a0-69c6-461e-b446-6362f5816a07", + "x": 0, + "y": 8, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incident History", + "description": "Every incident published in the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "eventType", + "title": "Event Type", + "renderer": { + "type": "text" + }, + "width": 170 + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "text" + }, + "width": 120 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "startTime", + "title": "Start Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "resolvedTime", + "title": "Resolved Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "timeToResolveMinutes", + "title": "Time To Resolve", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " min" + }, + "align": "right", + "width": 130 + }, + { + "field": "howFound", + "title": "How Found", + "renderer": { + "type": "text" + }, + "width": 160 + }, + { + "field": "customerImpact", + "title": "Customer Impact", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableSort": { + "field": "startTime", + "direction": "desc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "title", + "eventType", + "status", + "clouds", + "startTime", + "resolvedTime", + "timeToResolveMinutes", + "howFound", + "customerImpact", + "link" + ], + "hiddenColumns": [ + "id", + "type", + "phase", + "cloud", + "product", + "fedrampLevel", + "published", + "endTime", + "lastUpdated", + "durationMinutes", + "nextUpdate", + "workaround", + "summary", + "updateCount", + "latestUpdate" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Zscaler/v1/defaultContent/maintenanceAndAdvisories.dash.json b/plugins/Zscaler/v1/defaultContent/maintenanceAndAdvisories.dash.json new file mode 100644 index 00000000..ff85630e --- /dev/null +++ b/plugins/Zscaler/v1/defaultContent/maintenanceAndAdvisories.dash.json @@ -0,0 +1,708 @@ +{ + "name": "Maintenance & Advisories", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "ce63e2d8-4d24-4f58-b5e0-d52252fb6a77", + "x": 0, + "y": 0, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Upcoming & In Progress Maintenance", + "description": "Maintenance windows happening now or scheduled", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Maintenance" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Active" + }, + { + "column": "phase", + "operation": "equals", + "value": "Upcoming" + } + ] + }, + "sort": { + "by": [ + [ + "startTime", + "asc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "text" + }, + "width": 120 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "startTime", + "title": "Start Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "endTime", + "title": "End Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "durationMinutes", + "title": "Duration", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " min" + }, + "align": "right", + "width": 110 + } + ] + }, + "style": { + "richTableSort": { + "field": "startTime", + "direction": "asc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "title", + "status", + "clouds", + "startTime", + "endTime", + "durationMinutes", + "link" + ], + "hiddenColumns": [ + "id", + "type", + "eventType", + "phase", + "cloud", + "product", + "fedrampLevel", + "published", + "resolvedTime", + "lastUpdated", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "summary", + "updateCount", + "latestUpdate" + ] + } + } + } + } + }, + { + "i": "f28693bc-0770-401c-abe0-abc5926ea2cc", + "x": 0, + "y": 3, + "w": 1, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Maintenance By Cloud", + "description": "Maintenance windows in the timeframe affecting each cloud", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Maintenance" + ], + "oneRowPerCloud": true, + "dateField": "eventWindow" + }, + "group": { + "by": [ + [ + "cloud", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "count", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "cloud_uniqueValues", + "type": "nominal", + "title": "Cloud" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Maintenance Windows" + } + }, + "style": { + "cartesianHorizontal": true, + "showLabels": true + } + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "cloud_uniqueValues", + "yAxisData": [ + "count" + ], + "xAxisGroup": "none", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": false, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "horizontal", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "b56facb9-6fc5-4d91-93a0-d10ec5448e84", + "x": 1, + "y": 3, + "w": 3, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Maintenance History", + "description": "Maintenance windows that overlap the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Maintenance" + ], + "dateField": "eventWindow" + }, + "sort": { + "by": [ + [ + "startTime", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "text" + }, + "width": 120 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "startTime", + "title": "Start Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "endTime", + "title": "End Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "durationMinutes", + "title": "Duration", + "renderer": { + "type": "number", + "decimals": 0, + "suffix": " min" + }, + "align": "right", + "width": 110 + } + ] + }, + "style": { + "richTableSort": { + "field": "startTime", + "direction": "desc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "title", + "status", + "clouds", + "startTime", + "endTime", + "durationMinutes", + "link" + ], + "hiddenColumns": [ + "id", + "type", + "eventType", + "phase", + "cloud", + "product", + "fedrampLevel", + "published", + "resolvedTime", + "lastUpdated", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "summary", + "updateCount", + "latestUpdate" + ] + } + } + } + } + }, + { + "i": "88c53f5e-f0be-4d02-ba62-6acb7695d2ef", + "x": 0, + "y": 7, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Open Advisories", + "description": "Advisories that are still active", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Advisory" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Active" + } + ] + }, + "sort": { + "by": [ + [ + "published", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "eventType", + "title": "Event Type", + "renderer": { + "type": "text" + }, + "width": 170 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "published", + "title": "Published", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "summary", + "title": "Summary", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableSort": { + "field": "published", + "direction": "desc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "title", + "eventType", + "clouds", + "published", + "summary", + "link" + ], + "hiddenColumns": [ + "id", + "type", + "status", + "phase", + "state", + "cloud", + "product", + "fedrampLevel", + "startTime", + "endTime", + "resolvedTime", + "lastUpdated", + "durationMinutes", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "updateCount", + "latestUpdate" + ] + } + } + } + } + }, + { + "i": "8932cf8a-c136-47cb-b4c5-ec14ef69b396", + "x": 0, + "y": 10, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Advisories", + "description": "Advisories published in the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Advisory" + ] + }, + "sort": { + "by": [ + [ + "published", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "eventType", + "title": "Event Type", + "renderer": { + "type": "text" + }, + "width": 170 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "published", + "title": "Published", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "summary", + "title": "Summary", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableSort": { + "field": "published", + "direction": "desc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "title", + "eventType", + "clouds", + "published", + "summary", + "link" + ], + "hiddenColumns": [ + "id", + "type", + "status", + "phase", + "cloud", + "product", + "fedrampLevel", + "startTime", + "endTime", + "resolvedTime", + "lastUpdated", + "durationMinutes", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "updateCount", + "latestUpdate" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Zscaler/v1/defaultContent/manifest.json b/plugins/Zscaler/v1/defaultContent/manifest.json new file mode 100644 index 00000000..33a5323c --- /dev/null +++ b/plugins/Zscaler/v1/defaultContent/manifest.json @@ -0,0 +1,16 @@ +{ + "items": [ + { + "name": "overview", + "type": "dashboard" + }, + { + "name": "incidents", + "type": "dashboard" + }, + { + "name": "maintenanceAndAdvisories", + "type": "dashboard" + } + ] +} diff --git a/plugins/Zscaler/v1/defaultContent/overview.dash.json b/plugins/Zscaler/v1/defaultContent/overview.dash.json new file mode 100644 index 00000000..f55317d0 --- /dev/null +++ b/plugins/Zscaler/v1/defaultContent/overview.dash.json @@ -0,0 +1,786 @@ +{ + "name": "Overview", + "schemaVersion": "1.5", + "timeframe": "last30days", + "dashboard": { + "_type": "layout/grid", + "columns": 4, + "version": 1, + "contents": [ + { + "i": "7bfa9f96-8aa1-4341-8c48-aec0dbbd58a2", + "x": 0, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active Incidents", + "description": "Incidents that are still open", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Active" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Active Incidents" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Active Incidents" + } + } + } + } + }, + { + "i": "d20bedbe-a92b-479a-9c71-a4a7a4922f9d", + "x": 1, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Maintenance In Progress", + "description": "Maintenance windows happening now", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Maintenance" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Active" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Maintenance In Progress" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Maintenance In Progress" + } + } + } + } + }, + { + "i": "28ca8aac-c024-45ee-80cf-fdece891ed4b", + "x": 2, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Upcoming Maintenance", + "description": "Scheduled maintenance that has not started yet", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Maintenance" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Upcoming" + } + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Upcoming Maintenance" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Upcoming Maintenance" + } + } + } + } + }, + { + "i": "4a98a4d6-4fe3-41a7-87ce-6c57292a89c6", + "x": 3, + "y": 0, + "w": 1, + "h": 2, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Incidents", + "description": "Incidents published in the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident" + ] + }, + "group": { + "by": [], + "aggregate": [ + { + "type": "count" + } + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "scalar", + "encoding": { + "values": [ + { + "field": "count", + "type": "quantitative", + "title": "Incidents" + } + ] + }, + "style": { + "scalarLabelPosition": "none" + } + } + }, + "visualisation": { + "type": "data-stream-scalar", + "config": { + "data-stream-scalar": { + "value": "count", + "comparisonColumn": "none", + "label": "Incidents" + } + } + } + } + }, + { + "i": "f18c790d-4a2c-41dd-bf4a-9d7cca733794", + "x": 0, + "y": 2, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Cloud Health", + "description": "Current status of each Zscaler cloud", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[cloudStatus]}}", + "name": "cloudStatus", + "pluginConfigId": "{{configId}}" + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "statusGrid", + "encoding": { + "label": { + "field": "cloud", + "type": "nominal", + "title": "Cloud" + }, + "status": { + "field": "state", + "type": "nominal", + "title": "State" + }, + "value": { + "field": "statusText", + "type": "nominal", + "title": "Status" + }, + "link": { + "field": "currentEventLink", + "type": "nominal", + "title": "Current Event" + } + }, + "style": { + "statusGridColumnsPerRow": 5, + "statusGridOrderByField": "severity", + "statusGridOrderDirection": "desc", + "statusGridLabelWrap": true + } + } + }, + "visualisation": { + "type": "data-stream-blocks", + "config": { + "data-stream-blocks": { + "labelColumn": "cloud", + "stateColumn": "state", + "sublabel": "statusText", + "linkColumn": "none", + "columns": 5 + } + } + } + } + }, + { + "i": "4410cd07-8d16-44a6-91d3-3439d887f5fb", + "x": 0, + "y": 5, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Active & Upcoming Events", + "description": "Open incidents plus maintenance in progress or scheduled", + "timeframe": "none", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "dataSourceConfig": { + "type": [ + "Incident", + "Maintenance" + ] + }, + "filter": { + "multiOperation": "or", + "filters": [ + { + "column": "phase", + "operation": "equals", + "value": "Active" + }, + { + "column": "phase", + "operation": "equals", + "value": "Upcoming" + } + ] + }, + "sort": { + "by": [ + [ + "startTime", + "asc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "type", + "title": "Type", + "renderer": { + "type": "badge", + "colors": { + "Incident": "#ef4444", + "Maintenance": "#3b82f6", + "Advisory": "#f59e0b" + } + }, + "width": 120 + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "text" + }, + "width": 120 + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "startTime", + "title": "Start Time", + "renderer": { + "type": "date" + }, + "width": 170 + }, + { + "field": "latestUpdate", + "title": "Latest Update", + "renderer": { + "type": "text" + } + } + ] + }, + "style": { + "richTableSort": { + "field": "startTime", + "direction": "asc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "title", + "type", + "status", + "clouds", + "startTime", + "latestUpdate", + "link" + ], + "hiddenColumns": [ + "id", + "eventType", + "phase", + "cloud", + "product", + "fedrampLevel", + "published", + "endTime", + "resolvedTime", + "lastUpdated", + "durationMinutes", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "summary", + "updateCount" + ] + } + } + } + } + }, + { + "i": "875de933-bf7b-488f-9d4a-e0e6f85df327", + "x": 0, + "y": 8, + "w": 4, + "h": 3, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Posts Over Time", + "description": "Posts published per day by type", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "group": { + "by": [ + [ + "published", + "byDay" + ], + [ + "type", + "uniqueValues" + ] + ], + "aggregate": [ + { + "type": "count" + } + ] + }, + "sort": { + "by": [ + [ + "published_byDay", + "asc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "bar", + "encoding": { + "x": { + "field": "published_byDay", + "type": "temporal", + "title": "Published" + }, + "y": { + "field": "count", + "type": "quantitative", + "title": "Posts" + }, + "series": { + "field": "type_uniqueValues", + "type": "nominal", + "title": "Type" + } + }, + "style": { + "cartesianStack": "on", + "legendPosition": "bottom", + "mappedColors": { + "Incident": "#ef4444", + "Maintenance": "#3b82f6", + "Advisory": "#f59e0b" + } + } + } + }, + "visualisation": { + "type": "data-stream-bar-chart", + "config": { + "data-stream-bar-chart": { + "xAxisData": "published_byDay", + "yAxisData": [ + "count" + ], + "xAxisGroup": "type_uniqueValues", + "xAxisLabel": "", + "yAxisLabel": "", + "showXAxisLabel": false, + "showYAxisLabel": false, + "showLegend": true, + "legendPosition": "bottom", + "showGrid": true, + "horizontalLayout": "vertical", + "displayMode": "actual", + "showTotals": false, + "showValue": false, + "grouping": false, + "range": { + "type": "auto" + } + } + } + } + } + }, + { + "i": "10077e73-e6a0-4fa5-95ec-34ebbb60754d", + "x": 0, + "y": 11, + "w": 4, + "h": 4, + "moved": false, + "static": false, + "z": 0, + "config": { + "_type": "tile/data-stream", + "title": "Recent Posts", + "description": "Latest posts published in the timeframe", + "activePluginConfigIds": [ + "{{configId}}" + ], + "dataStream": { + "id": "{{dataStreams.[posts]}}", + "name": "posts", + "pluginConfigId": "{{configId}}", + "sort": { + "by": [ + [ + "published", + "desc" + ] + ] + } + }, + "vizSpec": { + "version": "1.0", + "visualization": { + "chartType": "richTable", + "encoding": { + "columns": [ + { + "field": "state", + "title": "State", + "renderer": { + "type": "status" + }, + "width": 90 + }, + { + "field": "published", + "title": "Published", + "renderer": { + "type": "date", + "relative": true + }, + "width": 130 + }, + { + "field": "type", + "title": "Type", + "renderer": { + "type": "badge", + "colors": { + "Incident": "#ef4444", + "Maintenance": "#3b82f6", + "Advisory": "#f59e0b" + } + }, + "width": 120 + }, + { + "field": "link", + "title": "Title", + "renderer": { + "type": "link", + "labelField": "title" + } + }, + { + "field": "clouds", + "title": "Clouds", + "renderer": { + "type": "tag-list", + "separator": ", " + }, + "width": 260 + }, + { + "field": "status", + "title": "Status", + "renderer": { + "type": "text" + }, + "width": 120 + } + ] + }, + "style": { + "richTableSort": { + "field": "published", + "direction": "desc" + } + } + } + }, + "visualisation": { + "type": "data-stream-table", + "config": { + "data-stream-table": { + "transpose": false, + "columnOrder": [ + "state", + "published", + "type", + "title", + "clouds", + "status", + "link" + ], + "hiddenColumns": [ + "id", + "eventType", + "phase", + "cloud", + "product", + "fedrampLevel", + "startTime", + "endTime", + "resolvedTime", + "lastUpdated", + "durationMinutes", + "timeToResolveMinutes", + "howFound", + "nextUpdate", + "customerImpact", + "workaround", + "summary", + "updateCount", + "latestUpdate" + ] + } + } + } + } + } + ] + } +} diff --git a/plugins/Zscaler/v1/docs/README.md b/plugins/Zscaler/v1/docs/README.md new file mode 100644 index 00000000..5ab2b6d7 --- /dev/null +++ b/plugins/Zscaler/v1/docs/README.md @@ -0,0 +1,91 @@ +Track Zscaler cloud incidents, scheduled maintenance and advisories in SquaredUp, using the public +RSS feed from the [Zscaler Trust](https://trust.zscaler.us) portal. The FedRAMP / Government portal +(`trust.zscaler.us`) and the Commercial portal ([trust.zscaler.com](https://trust.zscaler.com)) are +both supported. + +## Setup + +You don't need credentials, because the Trust portal feeds are public. + +1. Pick the **Trust portal** your Zscaler tenant is listed on: **FedRAMP / Government** for clouds + such as `zscalergov.net`, `zscalerten.net` and `zpagov.net`, or **Commercial** for clouds such + as `zscaler.net`, `zscalertwo.net` and `private.zscaler.com`. +2. Optional: to see only the clouds your organization uses, open the portal, note the cloud names + that appear at the end of post titles (for example `zscalergov.net`), and enter them in + **Cloud(s) to include**. +3. Save. The data source reads the feed once to check that it is reachable. + +## Configuration fields + +| Field | What it is | Where to find it | Required | +| ----- | ---------- | ---------------- | -------- | +| **Trust portal** | Which Zscaler Trust portal feed to read. | **FedRAMP / Government** is `trust.zscaler.us`; **Commercial** is `trust.zscaler.com`. | Yes | +| **Cloud(s) to include** | A comma-separated list of cloud names. Every tile is limited to posts that affect at least one of them. Leave it blank to include every cloud. | The cloud names at the end of each post title on the Trust portal, for example `zscalergov.net` | No | + +When you save, the data source reads the selected portal's RSS feed and checks that it returns a +valid RSS document. A failure means the portal could not be reached, or it returned something other +than its feed (for example a maintenance page). + +## What this plugin monitors + +- **Incidents**: service degradations, disruptions and issues under investigation. Each one includes + its status, the affected clouds, when it started and was resolved, the time to resolve, the + customer impact, any workaround, how it was found (**Internal Monitoring** or + **Customer Reported**), and the latest update text. +- **Scheduled maintenance**: maintenance windows that are upcoming, in progress or completed, with + start and end times, duration and the clouds affected. +- **Advisories**: notices such as end-of-life announcements, certificate changes and required + customer actions. +- **Cloud health**: the current state of each Zscaler cloud, rolled up from active incidents and + maintenance. + +The plugin includes three dashboards: **Overview**, **Incidents** and **Maintenance & Advisories**. + +## Data streams + +- **Posts**: one row per Trust portal post (incident, maintenance or advisory). It covers every + cloud on the selected portal, and has these parameters: + - **Type**: limits the rows to incidents, maintenance and/or advisories. + - **Cloud(s)**: limits the rows to posts that affect the selected clouds. + - **One row per affected cloud**: splits a post that affects several clouds into one row per + cloud, so that breakdowns by cloud count the post against each cloud it affects. + - **Timeframe applies to** (on the timeframe step): choose **Published** to filter by publish + date, or **Event window** to include any post whose start-to-end window overlaps the timeframe. + Set the timeframe to **None** to return everything in the feed. +- **Cloud Status**: one row per cloud, with its current state and counts of active incidents, + active and upcoming maintenance, and open advisories. It always shows current state, so it + doesn't support timeframes. + +The **State** column on both streams uses these rules: + +| State | Meaning | +| ----- | ------- | +| Error | An active incident with the event type **Service Disruption** (an outage) | +| Warning | Any other active incident (degradation, under investigation or monitoring), or maintenance in progress | +| Unknown | Upcoming maintenance, or an open advisory. Neither affects cloud health. | +| Success | Resolved, completed or cancelled | + +## What gets indexed + +Nothing. The feed contains status posts, not inventory, so this data source doesn't import any +objects into the SquaredUp graph. Clouds appear as row values, not as objects. + +## Known limitations + +- **History is about three months long.** The feed only contains the posts Zscaler currently + publishes, which is roughly the last three months. The plugin can't return anything older, and a + timeframe longer than that returns the same rows as **None**. +- **Every tile downloads the whole feed.** The feed has no server-side filtering or paging, so + every request fetches the full document (about 200 KB on the FedRAMP portal and about 1 MB on the + Commercial portal). All filtering happens after download. +- **Status is only as current as the portal.** Zscaler sometimes leaves a post **In Progress** long + after the issue has gone away. The plugin reports each post as it is published and doesn't guess + that a stale incident has ended, so one of these can keep a cloud at **Warning**. +- **Clouds are parsed from post titles.** The affected clouds come from the end of each title (for + example `- zscalergov.net (FedRAMP Moderate)`). If Zscaler changes that format, the plugin falls + back to the cloud in the post's link, which names only one cloud. +- **Advisory dates are date-only.** The portal publishes most advisories with a midnight (00:00 GMT) + timestamp. +- **Some text contains replacement characters.** A few posts on the portal include characters that + are already corrupted at the source (shown as `�`). The plugin passes them through unchanged. +- **Read-only.** The plugin only reads the public feed and never changes anything in Zscaler. diff --git a/plugins/Zscaler/v1/icon.svg b/plugins/Zscaler/v1/icon.svg new file mode 100644 index 00000000..62e1bb61 --- /dev/null +++ b/plugins/Zscaler/v1/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/plugins/Zscaler/v1/metadata.json b/plugins/Zscaler/v1/metadata.json new file mode 100644 index 00000000..5418fdd6 --- /dev/null +++ b/plugins/Zscaler/v1/metadata.json @@ -0,0 +1,36 @@ +{ + "name": "zscaler-trust", + "displayName": "Zscaler Trust Status", + "version": "1.0.0", + "author": { "name": "@shawn149", "type": "community" }, + "description": "Track Zscaler cloud incidents, scheduled maintenance and advisories from the Zscaler Trust portal.", + "category": "Monitoring", + "type": "hybrid", + "schemaVersion": "2.1", + "importNotSupported": true, + "restrictedToPlatforms": [], + "keywords": ["zscaler", "trust", "status", "incidents", "maintenance", "fedramp", "rss"], + "objectTypes": [], + "links": [ + { + "category": "documentation", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/ZScaler/v1/docs/README.md", + "label": "Help adding this plugin" + }, + { + "category": "source", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/ZScaler/v1", + "label": "Repository" + } + ], + "base": { + "plugin": "WebAPI", + "majorVersion": "1", + "config": { + "baseUrl": "{{typeof trustPortal !== 'undefined' && trustPortal ? trustPortal : 'https://trust.zscaler.us'}}", + "authMode": "none", + "headers": [], + "queryArgs": [] + } + } +} diff --git a/plugins/Zscaler/v1/ui.json b/plugins/Zscaler/v1/ui.json new file mode 100644 index 00000000..1d515c03 --- /dev/null +++ b/plugins/Zscaler/v1/ui.json @@ -0,0 +1,36 @@ +[ + { + "type": "markdown", + "name": "info", + "content": "Reads the public RSS feed of the [Zscaler Trust](https://trust.zscaler.us) portal. No credentials are needed." + }, + { + "type": "radio", + "name": "trustPortal", + "label": "Trust portal", + "defaultValue": "https://trust.zscaler.us", + "options": [ + { + "value": "https://trust.zscaler.us", + "label": "FedRAMP / Government (trust.zscaler.us)" + }, + { + "value": "https://trust.zscaler.com", + "label": "Commercial (trust.zscaler.com)" + } + ], + "validation": { + "required": true + } + }, + { + "type": "text", + "name": "clouds", + "label": "Cloud(s) to include", + "placeholder": "zscalergov.net, zpagov.net", + "help": "Limits every tile to posts that affect these clouds - comma-separated, matching the cloud names shown in post titles on the Trust portal. Leave blank to include every cloud.", + "validation": { + "required": false + } + } +] From 9cbbbbe923a9ee92fc61d6fb5c7d6242b748de20 Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 14:36:38 -0400 Subject: [PATCH 2/6] Fix Zscaler plugin links Correct the casing of the Zscaler plugin path in the metadata links so the documentation and repository URLs resolve correctly. --- plugins/Zscaler/v1/metadata.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/Zscaler/v1/metadata.json b/plugins/Zscaler/v1/metadata.json index 5418fdd6..ff04675d 100644 --- a/plugins/Zscaler/v1/metadata.json +++ b/plugins/Zscaler/v1/metadata.json @@ -14,12 +14,12 @@ "links": [ { "category": "documentation", - "url": "https://github.com/squaredup/plugins/blob/main/plugins/ZScaler/v1/docs/README.md", + "url": "https://github.com/squaredup/plugins/blob/main/plugins/Zscaler/v1/docs/README.md", "label": "Help adding this plugin" }, { "category": "source", - "url": "https://github.com/squaredup/plugins/tree/main/plugins/ZScaler/v1", + "url": "https://github.com/squaredup/plugins/tree/main/plugins/Zscaler/v1", "label": "Repository" } ], From ea28c17f1e9533ac4e71df2f84b1f8daddc1ba09 Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 14:38:09 -0400 Subject: [PATCH 3/6] Add Zscaler CODEOWNERS entry Co-Authored-By: Claude Opus 5.5 --- .github/CODEOWNERS | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 5fe6f243..45d1dc5d 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -15,6 +15,7 @@ plugins/IncidentIO/ @noorulkhan-n plugins/FantasyPremierLeague/* @TimWheeler-SQUP plugins/GoogleSheets/* @kieranlangton plugins/Ivanti-ITSM/ @shawn149 +plugins/Zscaler/ @shawn149 plugins/MetOffice/* @blackgrouse plugins/Phare/* @vinbab plugins/Postcoder/* @richbenwell From f2665846fddd493df8400b3797b03464bb9c28f9 Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 16:39:39 -0400 Subject: [PATCH 4/6] Fix Zscaler cloud status date parsing Normalize the incident `ResolvedDate` field in the Zscaler cloud status stream by converting it with `toDate()` instead of leaving it as plain text. This ensures resolved timestamps are handled correctly when processing incident data. --- plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js b/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js index d0e46f56..3e2b9c8f 100644 --- a/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js +++ b/plugins/Zscaler/v1/dataStreams/scripts/cloudStatus.js @@ -62,7 +62,7 @@ for (const item of channel.item || []) { const status = text(item.Status); const start = toDate(text(item.startTime)); const end = toDate(text(item.endTime)); - const resolved = text(item.ResolvedDate); + const resolved = toDate(text(item.ResolvedDate)); const pub = toDate(text(item.pubDate)); let phase; From f9699bc431ce151ed638c769add71652fb0c2b2c Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 16:44:41 -0400 Subject: [PATCH 5/6] Filter Zscaler posts by both cloud sources Posts now require a cloud to match both the data-source filter and the tile filter before being emitted. When one row per cloud is enabled, only matching clouds are expanded, and single-row output uses the first valid cloud instead of an excluded one. --- plugins/Zscaler/v1/dataStreams/scripts/posts.js | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/plugins/Zscaler/v1/dataStreams/scripts/posts.js b/plugins/Zscaler/v1/dataStreams/scripts/posts.js index 90e7d11b..59df8fc2 100644 --- a/plugins/Zscaler/v1/dataStreams/scripts/posts.js +++ b/plugins/Zscaler/v1/dataStreams/scripts/posts.js @@ -128,11 +128,17 @@ for (const item of channel.item || []) { const { headline, clouds } = parseTitle(text(item.title), link); const cloudNames = clouds.map((c) => c.cloud); - if (sourceClouds.length > 0 && !cloudNames.some((c) => sourceClouds.includes(c))) continue; + // A cloud must pass both the data-source and the tile filter, so a multi-cloud post never + // emits or labels its row with a cloud the user filtered out. + const matching = clouds.filter( + (c) => + (sourceClouds.length === 0 || sourceClouds.includes(c.cloud)) && + (wantedClouds.length === 0 || wantedClouds.includes(c.cloud)) + ); + if (matching.length === 0 && (sourceClouds.length > 0 || wantedClouds.length > 0)) continue; const type = TYPES[text(item.category)] || text(item.category); if (wantedTypes.length > 0 && !wantedTypes.includes(String(type).toLowerCase())) continue; - if (wantedClouds.length > 0 && !cloudNames.some((c) => wantedClouds.includes(c))) continue; const published = toDate(text(item.pubDate)); const startTime = toDate(text(item.startTime)); @@ -209,7 +215,7 @@ for (const item of channel.item || []) { }; if (oneRowPerCloud) { - for (const c of clouds) { + for (const c of matching) { rows.push({ ...base, id: `${base.id}-${c.cloud}`, @@ -221,8 +227,8 @@ for (const item of channel.item || []) { } else { rows.push({ ...base, - cloud: cloudNames[0], - product: cloudNames.length > 0 ? productOf(cloudNames[0]) : 'Other', + cloud: matching.length > 0 ? matching[0].cloud : undefined, + product: matching.length > 0 ? productOf(matching[0].cloud) : 'Other', fedrampLevel: levels.length > 0 ? levels.join(', ') : undefined }); } From 0a1f2c1d2e5dd5e519a5c5a9156d4769a7241b3c Mon Sep 17 00:00:00 2001 From: Shawn Williams Date: Tue, 6 Oct 2026 16:46:51 -0400 Subject: [PATCH 6/6] Rename Zscaler plugin to trust status Update the plugin metadata identifier to match the Zscaler Trust Status branding. This keeps the data source name consistent with its display name and avoids confusion in SquaredUp. --- plugins/Zscaler/v1/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/Zscaler/v1/metadata.json b/plugins/Zscaler/v1/metadata.json index ff04675d..0bcdd55c 100644 --- a/plugins/Zscaler/v1/metadata.json +++ b/plugins/Zscaler/v1/metadata.json @@ -1,5 +1,5 @@ { - "name": "zscaler-trust", + "name": "zscaler-trust-status", "displayName": "Zscaler Trust Status", "version": "1.0.0", "author": { "name": "@shawn149", "type": "community" },