Anyone who has automated against Cisco Secure Access (or reverse-engineered its internal telemetry endpoints) knows the frustration of the 30-minute token wall.
You are writing a Python automation script or validating policy updates against the Umbrella SSE endpoint at https://api.umbrella.com/v1/sse/organizations/{orgId}/rules. Because provisioning tenant-level API keys with administrative scopes requires a security exception, you do what every practitioner does during initial development: open Chrome DevTools, inspect the Network tab, copy the active Authorization: Bearer eyJhbGci... header from a dashboard API call, and paste it into your local terminal or Postman collection.
Your script runs cleanly. Ten minutes pass. You step away for coffee or dive into debugging a response schema. You run the script again:
HTTP/1.1 401 Unauthorized
{
"code": 401,
"message": "Token expired or invalid signature",
"status": "UNAUTHORIZED"
}
The dashboard's single sign-on (SSO) session uses short-lived JSON Web Tokens (JWTs) designed to rotate rapidly for security. While ephemeral tokens are a security best practice, having to constantly reopen DevTools, trigger page navigations, copy giant base64 strings, and paste them into development environments destroys engineering velocity.
Furthermore, when testing policy updates, copying the token is only half the battle. You also need to observe the exact JSON payload the dashboard sends when updating rules, and track whether new rules are registering hit counts in the cloud reporting engine.
To solve this, we built Secure Access Network Logger—a lightweight, dual-engine extension for Chrome (Manifest V3) and Firefox that silently captures live authorization headers and rule payloads in memory without console logging or DevTools friction.
The Security and Scoping Challenge
When designing a browser extension that handles authentication tokens, security discipline must come first. Extensions with overly broad permissions (such as <all_urls> host permissions combined with arbitrary token exfiltration) represent a massive supply-chain risk.
We established three immutable design rules for the harvester:
- Zero Exfiltration: The extension communicates exclusively with the local browser runtime. Captured tokens never leave local memory; there are no external analytics servers, cloud databases, or background webhooks.
- Strict URL Scoping: Token harvesting is completely dormant across the internet. It activates only when the browser tab matches the specific Cisco Secure Access policy path:
If an engineer navigates to another site—or even to another section of the Cisco dashboard—interception stops immediately.^https:\/\/dashboard\.sse\.cisco\.com\/org\/[^/]+\/secure\/policy(?:[/?#]|$) - No Console Pollution: The extension never logs authorization headers or bearer strings to
console.log. Emitting sensitive credentials into browser developer consoles exposes tokens to screen-sharing sessions, automated error reporters, and local session replays.
Dual-Layer Architecture: WebRequest vs. Injected Script
In modern browser extension architectures (especially Manifest V3), inspecting network traffic presents a technical hurdle:
chrome.webRequestcan inspect HTTP headers (including the outboundAuthorizationheader), but in Manifest V3 background service workers,webRequestcannot inspect the body of outbound requests or the response body of dynamicfetch()calls.- Content Scripts run in an isolated JavaScript world. They have access to the page DOM, but they cannot hook into the page's native
window.fetchorwindow.XMLHttpRequestprototypes because the web page's JavaScript execution context is isolated from the extension context.
To solve this, the extension uses a coordinated two-tier interception architecture:
[Page Context: MAIN World]
│
├─► injected.js (Wraps window.fetch & XMLHttpRequest)
│ │
│ ├─► Intercepts: POST/GET api.umbrella.com/v1/sse/.../rules
│ ├─► Serializes: method, url, requestBody, response JSON
│ │
│ ▼ (window.postMessage)
[Content Script: ISOLATED World]
│
├─► content.js (Receives message, validates origin)
│ │
│ ▼ (chrome.runtime.sendMessage)
[Background Service Worker]
│
├─► background.js
│ ├─► chrome.webRequest.onSendHeaders: Sniffs live "Authorization" header
│ └─► Stores latest token and rules payload in volatile memory
1. Injected Script: Intercepting Rule Payloads
injected.js is loaded directly into the DOM by content.js, giving it native access to the dashboard's JavaScript runtime. It patches window.fetch to intercept calls to the rules endpoint:
(function interceptRulesEndpoint() {
const endpointRegex = /^https:\/\/api\.umbrella\.com\/v1\/sse\/organizations\/[^/]+\/rules(?:[/?#]|$)/i;
function isTargetUrl(url) {
if (typeof url !== "string" || !endpointRegex.test(url)) return false;
try {
const parsed = new URL(url, window.location.origin);
// Capture the primary non-default rules query
return parsed.searchParams.get("ruleIsDefault") === "false" &&
parsed.searchParams.get("offset") === "0";
} catch (_) {
return false;
}
}
const originalFetch = window.fetch;
window.fetch = async function wrappedFetch(input, init) {
const url = typeof input === "string" ? input : (input && input.url);
const method = (init && init.method) || "GET";
if (isTargetUrl(url)) {
const requestBody = serializeBody(init && init.body);
try {
const response = await originalFetch.apply(this, arguments);
const cloned = response.clone();
cloned.text().then((text) => {
const parsed = safeJsonParse(text);
emit({
url,
method,
requestBody,
rules: parsed && parsed.rules ? parsed.rules : null,
rawResponse: text
});
});
return response;
} catch (err) {
throw err;
}
}
return originalFetch.apply(this, arguments);
};
})();
When the dashboard fetches its policy rules, injected.js clones the response stream, parses the JSON payload, and emits an event via window.postMessage. The content script catches the event and forwards it to the background worker.
2. Background Worker: Capturing the Bearer Token
Simultaneously, background.js uses webRequest.onSendHeaders to harvest the exact Authorization header sent with the request:
api.webRequest.onSendHeaders.addListener(
(details) => {
if (!details || details.tabId < 0) return;
if (!policyTabIds.has(details.tabId)) return; // Only process verified policy tabs
if (!apiRequestTypes.has(details.type)) return;
const authHeader = findAuthorizationHeader(details.requestHeaders);
if (!authHeader || !authHeader.value) return;
// Store in volatile memory
lastApiAuthorization = {
...baseRecord(details),
authorization: authHeader.value, // "Bearer eyJhbGciOi..."
};
},
allUrlsFilter,
["requestHeaders"]
);
By correlating the tab state, the extension captures the live bearer token without interfering with the network request or modifying headers in transit.
Programmatic Access: The Extension Messaging Contract
Once the token and rules payload are stored in the extension's memory, other tools, popups, or local automation harnesses can query the background service worker using standard WebExtension messaging:
1. Reading the Latest Authorization Token
chrome.runtime.sendMessage({ action: "getLastApiAuthorization" }, (response) => {
if (response && response.ok && response.authorization) {
console.log("Captured Token:", response.authorization.authorization);
console.log("Captured At:", response.authorization.timestamp);
}
});
Response payload:
{
"ok": true,
"authorization": {
"requestId": "184209",
"url": "https://api.umbrella.com/v1/sse/organizations/942183/rules",
"method": "GET",
"tabId": 14,
"type": "fetch",
"timestamp": "2026-10-11T14:35:12.104Z",
"authorization": "Bearer eyJhbGciOiJSUzI1NiIsImtpZCI6..."
}
}
2. Reading the Live Rules Payload
chrome.runtime.sendMessage({ action: "getLastRulesEndpointCapture" }, (response) => {
if (response && response.ok && response.capture) {
console.log("Rule Count:", response.capture.rules.length);
}
});
Real-Time Telemetry: Polling Rule Hit Counts
Capturing rules and tokens also enabled a critical operational feature: real-time rule verification.
When a network engineer adds a new firewall rule in Cisco Secure Access to block a malicious IP or allow a partner branch, how do they verify that the rule is actually processing traffic? In the default UI, you must wait, refresh the page, or navigate to Activity Search logs.
The network logger integrates directly with Cisco's reporting backend at https://api.us.reports.umbrella.com.
When an engineer selects a rule in the extension UI, the extension records the ruleId and begins polling the hit count API every 10 seconds:
async function fetchHitcountForRecordedRule() {
if (!lastRecordedRule || !lastApiAuthorization) {
return { ok: false, error: "No recorded rule or active token" };
}
const { organizationId, ruleId } = lastRecordedRule;
const token = lastApiAuthorization.authorization;
const url = `${reportsApiBase}/v1/organizations/${organizationId}/reports/rules/${ruleId}/hitcount`;
const response = await fetch(url, {
headers: {
"Authorization": token,
"Accept": "application/json"
}
});
if (!response.ok) {
return { ok: false, status: response.status };
}
const data = await response.json();
return { ok: true, hitcount: data.hitcount, lastUpdated: new Date().toISOString() };
}
This provides immediate visual confirmation right in the browser overlay: as test packets hit the cloud firewall, the hit count increments live on screen.
Cross-Browser Packaging: Chrome MV3 vs. Firefox WebExtensions
The extension is engineered to run in both Google Chrome and Mozilla Firefox. Because Chrome enforces Manifest V3 (MV3) with background service workers, while Firefox supports event pages and background scripts, the repository provides two manifests:
manifest.chrome.json:{ "manifest_version": 3, "name": "Network Request Logger", "background": { "service_worker": "background.js" } }manifest.json(Firefox compatible):{ "manifest_version": 3, "name": "Network Request Logger", "background": { "scripts": ["background.js"] } }
Both environments share the exact same content.js, injected.js, and background.js logic through an abstraction shim:
const api = typeof browser !== "undefined" ? browser : chrome;
Installation and Quick Start
Running in Google Chrome
- Clone the repository:
git clone https://github.com/technoxi/secure-access-network-logger.git cd secure-access-network-logger - Copy the Chrome manifest:
cp manifest.chrome.json manifest.json - Open Chrome and navigate to
chrome://extensions. - Enable Developer mode in the upper-right corner.
- Click Load unpacked and select the repository directory.
Running in Mozilla Firefox
- Open Firefox and navigate to
about:debugging#/runtime/this-firefox. - Click Load Temporary Add-on...
- Select
manifest.jsondirectly from the repository.
Verifying Interception
- Log in to Cisco Secure Access and navigate to:
https://dashboard.sse.cisco.com/org/<your-org-id>/secure/policy - Open the browser DevTools console for the extension service worker (from the
chrome://extensionscard). - The extension is now actively monitoring. Execute:
You will receive the live bearer token and organization context immediately.chrome.runtime.sendMessage({ action: "getLastApiAuthorization" }, console.log);
Engineering Takeaways
Automating enterprise SaaS platforms does not always require waiting for comprehensive API coverage or cumbersome authentication provisioning.
- Scoped interception is secure: By scoping webRequest listeners strictly to specific URLs and storing credentials exclusively in volatile memory, engineers can build powerful development tooling without compromising credential hygiene.
- Combine webRequest and prototype hooks: When Manifest V3 limits service workers from reading response bodies, injecting an in-page script allows you to capture API responses while using webRequest for headers.
- Turn passive logs into active verification: Capturing rule identifiers in real time and tying them to cloud reporting APIs turns blind policy authoring into an observable, verified operational feedback loop.