During a SOC 2 Type II audit earlier this year, our compliance auditor asked for a complete list of sanctioned, monitored, and blocked SaaS applications configured in our security policy.
For teams running Cisco Secure Access (formerly Umbrella with SSE), this sounds like a trivial five-minute request: open the dashboard, navigate to Resources > Internet and SaaS Resources > Application Lists, and click "Export."
Except there is no "Export" button.
What you get instead is an interactive table displaying more than 10,200 applications grouped across dozens of categories, paginated at 20 rows per screen. If an auditor wants to review the full catalog—or if a security operations team needs to verify whether newly discovered AI tools, shadow cloud storage, or unsanctioned collaboration platforms are explicitly classified—you are left with an unworkable choice: spend hours clicking "Next Page," or inspect network traffic in Chrome DevTools to grab ad-hoc JSON chunks.
Earlier this year, we wrote about capturing that payload manually using browser developer tools. It worked for an emergency triage session, but manual DevTools inspection is too brittle for recurring audits, quarterly access reviews, or automated CMDB syncs.
To eliminate the manual friction, we built Secure Access Application Exporter—a dedicated, open-source Chrome extension that pulls the entire application catalog, category definitions, enterprise apps, and custom organization lists into a single consolidated JSON file with one click.
The Dual-API Architecture Problem
When engineers decide to script an export like this, their first instinct is to head to Cisco's developer portal, generate an API key, and write a Python script.
That is where the first wall appears. Cisco Secure Access is the culmination of several historical cloud platforms merged under a unified interface. Underneath the sleek modern frontend, the dashboard talks to two distinct backend API estates:
- The Legacy OpenDNS Core (
api.opendns.com):- Serves the master application catalog (over 10,200 global applications and protocols).
- Serves the category hierarchy (64+ content and security categories with bitmask IDs).
- Serves tenant-specific enterprise applications.
- The Cloud SSE Platform (
api.umbrella.com):- Serves modern access rules and tenant-specific Application Lists.
- Handles modern identity mappings, private access policies, and cloud firewall definitions.
These two API estates do not share a single public API key scope. A read-only API key generated in the Secure Access dashboard often has permission for api.umbrella.com/v1/sse/ but receives a 403 Forbidden when attempting to query api.opendns.com/v3/organizations/{orgId}/applications.
Furthermore, in many enterprises, getting security administration approval to generate a permanent API credential with tenant-wide read scopes can take weeks through change advisory boards.
The Solution: Leveraging the Active Dashboard Session
The irony is that an engineer sitting in front of their browser already holds all the necessary credentials. The active web session has already passed SSO, MFA, and tenant authorization checks.
The dashboard frontend solves the dual-API problem dynamically:
- To talk to
api.opendns.com, the dashboard frontend makes a background request to an internal/tokenendpoint with session cookies (credentials: "include"), obtaining a short-lived bearer token. - To talk to
api.umbrella.com, the dashboard stores a signed JSON Web Token (JWT) in browser storage (sessionStorageorlocalStorage) after authenticating.
Our extension takes advantage of this by running a lightweight collector function inside the dashboard page context (MAIN execution world) using chrome.scripting.executeScript.
[Browser Extension Popup]
│ (chrome.runtime.sendMessage { type: "EXPORT" })
▼
[Extension Service Worker]
│ (chrome.scripting.executeScript in MAIN world)
▼
[Active Dashboard Tab (dashboard.sse.cisco.com)]
├─► Fetch /token ───────────────────────► Bearer Token (api.opendns.com)
├─► Read sessionStorage/localStorage ──► Cached JWT (api.umbrella.com)
│
├─► GET api.opendns.com/.../applications
├─► GET api.opendns.com/.../applicationcategories
├─► GET api.opendns.com/.../enterpriseapplications
└─► GET api.umbrella.com/.../application_lists
│
▼
[Consolidated JSON Download] ──► secure-access-applications-<orgId>-<date>.json
Because the collector executes directly within the dashboard tab, all outbound requests carry the dashboard's own origin, cookies, and CORS context. Zero credentials leave the browser, zero external servers are involved, and no permanent API keys need to be provisioned.
Token Harvesting and In-Page Extraction
The technical engine of the extension is the collectInPage() function in service-worker.js.
To obtain the api.opendns.com token, the script calls the internal session endpoint directly:
async function opendnsToken() {
try {
const response = await fetch("/token", {
credentials: "include",
headers: { Accept: "application/json" }
});
if (response.ok) {
const body = await response.json();
if (body && body.token) return body.token;
}
} catch (_) {}
// Fallback to storage if /token fails
const cached = storedJwt(/api\.opendns\.com/);
if (cached) return cached;
throw new Error("Could not acquire token for api.opendns.com. Please reload the dashboard.");
}
For the SSE token targeting api.umbrella.com, the script inspects browser storage. Because single-page applications frequently store nested state objects, the harvester scans both sessionStorage and localStorage, extracting candidates that match JWT structure (header.payload.signature), decoding the payload claims, and ensuring the token has not expired:
function storedJwt(pattern) {
const JWT_REGEX = /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/;
const now = Date.now() / 1000;
let bestToken = null;
for (const store of [sessionStorage, localStorage]) {
for (let i = 0; i < store.length; i++) {
const key = store.key(i);
const raw = store.getItem(key) || "";
const candidates = [];
if (JWT_REGEX.test(raw.trim())) candidates.push(raw.trim());
try {
const parsed = JSON.parse(raw);
if (parsed && typeof parsed === "object") {
for (const val of Object.values(parsed)) {
if (typeof val === "string" && JWT_REGEX.test(val.trim())) {
candidates.push(val.trim());
}
}
}
} catch (_) {}
for (const token of candidates) {
const claims = decodeJwtClaims(token) || {};
const audience = [].concat(claims.aud || [], claims.iss || []).join(" ");
if (!pattern.test(audience) && !pattern.test(key)) continue;
if (claims.exp && claims.exp < now + 5) continue; // Skip expired
if (!bestToken || (claims.exp || 0) > bestToken.exp) {
bestToken = { token, exp: claims.exp || 0 };
}
}
}
}
return bestToken && bestToken.token;
}
Once both tokens are in hand, the collector dispatches parallel HTTP queries against the four endpoints:
const CATALOGS = {
applications: {
token: "opendns",
url: (orgId) => `https://api.opendns.com/v3/organizations/${orgId}/applications?outputFormat=jsonHttpStatusOverride`,
dataKey: "data"
},
applicationCategories: {
token: "opendns",
url: (orgId) => `https://api.opendns.com/v3/organizations/${orgId}/applicationcategories?optionalFields=%5B%22applicationsCount%22%5D&outputFormat=jsonHttpStatusOverride`,
dataKey: "data"
},
enterpriseApplications: {
token: "opendns",
url: (orgId) => `https://api.opendns.com/v3/organizations/${orgId}/enterpriseapplications?outputFormat=jsonHttpStatusOverride`,
dataKey: "data"
},
applicationLists: {
token: "sse",
url: (orgId) => `https://api.umbrella.com/v1/organizations/${orgId}/application_lists`,
dataKey: "applicationLists"
}
};
Anatomy of the Export Artifact
When the export finishes, the extension triggers a native browser download (chrome.downloads.download). The downloaded file follows the naming standard:
secure-access-applications-<orgId>-<YYYY-MM-DD-HH-mm-ss>.json
The resulting JSON structure isolates errors cleanly. If one catalog fails (for example, if an organization has not configured any custom enterprise applications), the remaining three catalogs still export successfully, and the error reason is recorded in the header:
{
"exportedAt": "2026-10-11T14:20:00.000Z",
"orgId": "942183",
"source": "dashboard.sse.cisco.com",
"counts": {
"applications": 10213,
"applicationCategories": 64,
"enterpriseApplications": 12,
"applicationLists": 8
},
"errors": {},
"applications": [
{
"id": 1420,
"name": "ChatGPT",
"category": { "id": 48, "name": "Generative AI" },
"description": "OpenAI conversational AI and LLM platform",
"visibility": "PUBLIC",
"protocols": ["HTTPS"]
}
],
"applicationCategories": [
{
"id": 48,
"name": "Generative AI",
"applicationsCount": 84,
"isSecurityCategory": false
}
],
"enterpriseApplications": [],
"applicationLists": [
{
"id": 302,
"name": "Sanctioned Developer Tools",
"applicationIds": [12, 450, 912],
"createdAt": "2025-11-14T09:12:00Z"
}
]
}
Operational Use Cases
Having direct access to this structured export transforms multiple security and compliance operations:
1. Shadow IT and Generative AI Governance
When organizations update their Acceptable Use Policy (AUP) to restrict unvetted Generative AI tools, security analysts need to know every single app Cisco classifies under Category 48 ("Generative AI").
Instead of clicking through the UI to see which tools exist, a one-line jq filter over the export reveals all matching entries:
jq '.applications[] | select(.category.name == "Generative AI") | {id, name}' secure-access-applications.json
Analysts can immediately cross-reference this list with their enterprise single sign-on (SSO) catalog to spot unsanctioned tools.
2. Multi-Tenant Staging vs. Production Auditing
Many organizations maintain a staging tenant in Cisco Secure Access to validate policies before pushing to production. Reconciling whether custom Application Lists match between staging and production is notoriously error-prone when done by eye.
By taking an export from both tenants, a simple diff script highlights missing application IDs in seconds:
diff <(jq -S '.applicationLists[] | {name, applicationIds}' staging.json) \
<(jq -S '.applicationLists[] | {name, applicationIds}' prod.json)
3. SIEM and Threat Hunting Enrichment
Security Information and Event Management (SIEM) systems ingesting Cisco Secure Access connection events receive numeric application IDs and category codes in raw syslog or S3 bucket logs.
By loading secure-access-applications.json as a lookup table into Splunk, Microsoft Sentinel, or Snowflake, threat hunters can translate raw IDs into human-readable application names and risk categories during incident investigation.
Installation and Usage Guide
The extension is lightweight (fewer than 200 lines of JavaScript) and requires no build step:
1. Installation
- Clone the repository:
git clone https://github.com/technoxi/secure-access-app-exporter.git - Open Google Chrome and navigate to
chrome://extensions. - Toggle Developer mode on in the top-right corner.
- Click Load unpacked and select the
extension/folder.
2. Running an Export
- Log in to your Cisco Secure Access tenant at
https://dashboard.sse.cisco.com. - Open any page under your organization (e.g.,
https://dashboard.sse.cisco.com/org/1234567/secure/policy). - Click the Secure Access Application Exporter icon in the Chrome toolbar.
- Keep the catalogs you need checked (Applications, Categories, Enterprise Apps, Application Lists).
- Click Export JSON.
- The file will be generated and saved directly to your browser's default Downloads directory.
3. Common Troubleshooting Scenarios
- "Open the Secure Access dashboard ... first": The extension did not detect an active tab matching
dashboard.sse.cisco.com. Open the dashboard in the same window. - "no api.umbrella.com token in this dashboard tab": Navigate to Secure > Policy > Rules once so the frontend loads its SSE JWT into local storage, then click Export again.
- HTTP 401/403 Errors: Your dashboard session has timed out. Simply refresh the browser tab, authenticate through your SSO identity provider, and re-run the export.
Engineering Takeaways
Security platforms manage massive amounts of critical configuration data, but web interfaces are designed for human navigation, not programmatic auditing.
- The browser is an authenticated client: When public APIs are restricted or difficult to provision, an engineer's authenticated browser session already holds the identity and authorization needed to retrieve configuration data cleanly.
- Handle backend fragmentation transparently: Merged enterprise platforms frequently communicate with multiple legacy backends. Recognizing which endpoint requires which token format is the difference between broken automation and a seamless export.
- Data portability improves security: Getting application catalogs into structured JSON allows security teams to verify policies, audit shadow IT, and feed detection pipelines without being trapped behind pagination controls.