Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions __mocks__/webextension-polyfill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,16 @@ const Browser = {
},
tabs: {
query: jest.fn(),
sendMessage: jest.fn(),
onCreated: {
addListener: jest.fn(),
},
onUpdated: {
addListener: jest.fn(),
},
onRemoved: {
addListener: jest.fn(),
},
},
};

Expand Down
101 changes: 95 additions & 6 deletions source/Background/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,13 @@ import 'emoji-log';
import Browser, {Cookies, Runtime} from 'webextension-polyfill';
import {ReportedStorage} from '../types/ReportedModel';
import {ZestScript, ZestScriptMessage} from '../types/zestScript/ZestScript';
import {ZestStatementWindowClose} from '../types/zestScript/ZestStatement';
import {
ZestStatementActionSleep,
ZestStatementWindowClose,
ZestStatementWindowHandle,
} from '../types/zestScript/ZestStatement';
import {
DEFAULT_WINDOW_HANDLE,
GET_ZEST_SCRIPT,
IS_FULL_EXTENSION,
LOCAL_STORAGE,
Expand All @@ -31,6 +36,7 @@ import {
RESET_ZEST_SCRIPT,
SESSION_STORAGE,
STOP_RECORDING,
ZAP_REGISTER_POPUP,
ZEST_SCRIPT,
} from '../utils/constants';

Expand All @@ -41,6 +47,16 @@ console.log('ZAP Service Worker 👋');
*/
const reportedStorage = new Set<string>();
const zestScript = new ZestScript();

let windowHandleCounter = 1;
const popupTabHandles = new Map<number, string>();

const POPUP_WINDOW_SLEEP_MS = 10000;

interface RegisterPopupMessage {
type: string;
url?: string;
}
/*
A callback URL will only be available if the browser has been launched from ZAP, otherwise call the individual endpoints
*/
Expand Down Expand Up @@ -233,6 +249,8 @@ async function handleMessage(

case RESET_ZEST_SCRIPT:
zestScript.reset();
windowHandleCounter = 1;
popupTabHandles.clear();
break;

case STOP_RECORDING: {
Expand All @@ -246,6 +264,8 @@ async function handleMessage(
sendZestScriptToZAP(data, zapkey, zapurl);
}
}
windowHandleCounter = 1;
popupTabHandles.clear();
break;
}

Expand All @@ -260,19 +280,72 @@ async function handleMessage(
async function onMessageHandler(
message: unknown,
_sender: Runtime.MessageSender
): Promise<number | ZestScriptMessage> {
): Promise<number | ZestScriptMessage | string> {
const msg = message as MessageEvent;

if (msg.type === ZAP_REGISTER_POPUP) {
// Called by the content script when it detects window.opener !== null.
// Assign a new window handle for this popup tab if not already registered.
const tabId = _sender.tab?.id;
const popupUrl = (msg as unknown as RegisterPopupMessage).url ?? '';

if (tabId !== undefined) {
const cached = popupTabHandles.get(tabId);
if (cached !== undefined) {
return Promise.resolve(cached);
}
windowHandleCounter += 1;
const handle = `windowHandle${windowHandleCounter}`;
popupTabHandles.set(tabId, handle);

// Emit ZestClientWindowHandle so ZAP's runner can locate and register
// this popup window by URL before replaying any popup interactions.
// Use regex=true with the origin (scheme+host) so that URL parameters
// that change between sessions (nonce, state, etc.) don't break the match.
let urlPattern = '.*';
try {
const parsed = new URL(popupUrl);
urlPattern = `${parsed.origin.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}.*`;
} catch {
// popupUrl is empty or invalid; fall back to match-all
}
const sleepStmt = new ZestStatementActionSleep(POPUP_WINDOW_SLEEP_MS);
const sleepStmtData = zestScript.addStatement(sleepStmt.toJSON());
const stmt = new ZestStatementWindowHandle(handle, urlPattern, true);
const stmtData = zestScript.addStatement(stmt.toJSON());
if (IS_FULL_EXTENSION) {
const items = await Browser.storage.sync.get({
zapurl: 'http://zap/',
zapkey: 'not set',
});
sendZestScriptToZAP(
sleepStmtData,
items.zapkey as string,
items.zapurl as string
);
sendZestScriptToZAP(
stmtData,
items.zapkey as string,
items.zapurl as string
);
}

return Promise.resolve(handle);
}
return Promise.resolve(DEFAULT_WINDOW_HANDLE);
}
let val: number | ZestScriptMessage = 2;
const items = await Browser.storage.sync.get({
zapurl: 'http://zap/',
zapkey: 'not set',
});
const msg = await handleMessage(
message as MessageEvent,
const result = await handleMessage(
msg,
items.zapurl as string,
items.zapkey as string
);
if (!(typeof msg === 'boolean')) {
val = msg;
if (!(typeof result === 'boolean')) {
val = result;
}
return Promise.resolve(val);
}
Expand All @@ -296,6 +369,22 @@ function cookieChangeHandler(

Browser.runtime.onMessage.addListener(onMessageHandler);

Browser.tabs.onRemoved.addListener(async (tabId) => {
const handle = popupTabHandles.get(tabId);
if (!handle) return;
popupTabHandles.delete(tabId);

if (IS_FULL_EXTENSION) {
const items = await Browser.storage.sync.get({
zapurl: 'http://zap/',
zapkey: 'not set',
});
const stmt = new ZestStatementWindowClose(0, handle);
const data = zestScript.addStatement(stmt.toJSON());
sendZestScriptToZAP(data, items.zapkey as string, items.zapurl as string);
}
});

if (IS_FULL_EXTENSION) {
Browser.action.onClicked.addListener((_tab: Browser.Tabs.Tab) => {
Browser.runtime.openOptionsPage();
Expand Down
45 changes: 33 additions & 12 deletions source/ContentScript/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
} from '../types/ReportedModel';
import Recorder from './recorder';
import {
DEFAULT_WINDOW_HANDLE,
IS_FULL_EXTENSION,
LOCAL_STORAGE,
LOCAL_ZAP_ENABLE,
Expand All @@ -35,6 +36,7 @@ import {
SESSION_STORAGE,
URL_ZAP_ENABLE,
URL_ZAP_RECORD,
ZAP_REGISTER_POPUP,
ZAP_START_RECORDING,
ZAP_STOP_RECORDING,
} from '../utils/constants';
Expand Down Expand Up @@ -302,18 +304,37 @@ function injectScript(): Promise<boolean> {
return new Promise((resolve) => {
configureExtension();
withZapRecordingActive(() => {
Browser.storage.sync
.get({initScript: false, loginUrl: '', startTime: 0})
.then((items) => {
console.log(
`ZAP injectScript items ${items.initScript} ${items.loginUrl}`
);
recorder.recordUserInteractions(
items.initScript === true,
items.loginUrl as string,
items.startTime as number
);
});
// Detect popup windows using window.opener: if non-null, this page was
// opened via window.open() (e.g. OAuth login popup). This works in all
// modes (private, normal) and all browsers, unlike background-side detection.
const isWindowPopup = window.opener !== null;
const handlePromise = isWindowPopup
? Browser.runtime
.sendMessage({type: ZAP_REGISTER_POPUP, url: window.location.href})
.catch(() => DEFAULT_WINDOW_HANDLE)
: Promise.resolve(DEFAULT_WINDOW_HANDLE);

Promise.all([
Browser.storage.sync.get({
initScript: false,
loginUrl: '',
startTime: 0,
}),
handlePromise,
]).then(([items, handle]) => {
const windowHandle = (handle as string) || DEFAULT_WINDOW_HANDLE;
console.log(
`ZAP injectScript items ${items.initScript} ${items.loginUrl} handle=${windowHandle} popup=${isWindowPopup}`
);
if (isWindowPopup) {
recorder.setWindowHandle(windowHandle);
}
recorder.recordUserInteractions(
isWindowPopup ? false : items.initScript === true,
items.loginUrl as string,
items.startTime as number
);
});
});
withZapEnableSetting(() => {
enableExtension();
Expand Down
40 changes: 27 additions & 13 deletions source/ContentScript/recorder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {ZestScriptMessage} from '../types/zestScript/ZestScript';
import {getPath} from './util';
import {downloadJson} from '../utils/util';
import {
DEFAULT_WINDOW_HANDLE,
GET_ZEST_SCRIPT,
STOP_RECORDING,
ZAP_FLOATING_DIV,
Expand All @@ -57,6 +58,8 @@ class Recorder {

active = false;

windowHandle: string = DEFAULT_WINDOW_HANDLE;

haveListenersBeenAdded = false;

floatingWindowInserted = false;
Expand Down Expand Up @@ -104,7 +107,11 @@ class Recorder {

sendScrollToToZap(elementLocator: ElementLocator, waitForMsec: number): void {
this.sendZestScriptToZAP(
new ZestStatementElementScrollTo(elementLocator, waitForMsec),
new ZestStatementElementScrollTo(
elementLocator,
waitForMsec,
this.windowHandle
),
{sendCache: false, notify: false}
);
}
Expand All @@ -131,20 +138,20 @@ class Recorder {
}
if (this.curLevel > level) {
while (this.curLevel > level) {
this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1), {
sendCache: true,
notify: true,
});
this.sendZestScriptToZAP(
new ZestStatementSwitchToFrame(-1, '', this.windowHandle),
{sendCache: true, notify: true}
);
this.curLevel -= 1;
}
this.curFrame = frameIndex;
} else {
this.curLevel += 1;
this.curFrame = frameIndex;
this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex), {
sendCache: true,
notify: true,
});
this.sendZestScriptToZAP(
new ZestStatementSwitchToFrame(frameIndex, '', this.windowHandle),
{sendCache: true, notify: true}
);
}
if (this.curLevel !== level) {
console.log('ZAP Error in switching frames');
Expand Down Expand Up @@ -172,7 +179,7 @@ class Recorder {

this.sendScrollToToZap(elementLocator, waited);
this.sendZestScriptToZAP(
new ZestStatementElementClick(elementLocator, waited),
new ZestStatementElementClick(elementLocator, waited, this.windowHandle),
{sendCache: true, notify: true}
);
// click on target element
Expand Down Expand Up @@ -225,7 +232,8 @@ class Recorder {
new ZestStatementElementSendKeys(
elementLocator,
(event.target as HTMLInputElement).value,
waited
waited,
this.windowHandle
),
{sendCache: false, notify: true}
);
Expand All @@ -250,7 +258,8 @@ class Recorder {
// Cache the statement as it often occurs before the change event occurs
this.cachedSubmit = new ZestStatementElementSubmit(
elementLocator,
this.getWaited()
this.getWaited(),
this.windowHandle
);
this.cachedTimeStamp = event.timeStamp;
// console.log('ZAP Caching submit', this.cachedSubmit);
Expand Down Expand Up @@ -485,7 +494,8 @@ class Recorder {
this.sendZestScriptToZAP(
new ZestStatementLaunchBrowser(
this.getBrowserName(),
loginUrl !== '' ? loginUrl : window.location.href
loginUrl !== '' ? loginUrl : window.location.href,
this.windowHandle
),
{sendCache: true, notify: true}
);
Expand Down Expand Up @@ -771,6 +781,10 @@ class Recorder {
}, 100);
});
}

setWindowHandle(handle: string): void {
this.windowHandle = handle;
}
}

export default Recorder;
2 changes: 1 addition & 1 deletion source/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "ZAP by Checkmarx Browser Extension",
"version": "0.1.8",
"version": "0.1.9",

"icons": {
"16": "assets/icons/zap16x16.png",
Expand Down
5 changes: 3 additions & 2 deletions source/manifest.rec.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"manifest_version": 3,
"name": "ZAP by Checkmarx Recorder",
"version": "0.1.8",
"version": "0.1.9",

"icons": {
"16": "assets/icons/zap16x16.png",
Expand All @@ -16,7 +16,8 @@
"incognito": "spanning",

"permissions": [
"storage"
"storage",
"tabs"
],

"host_permissions": [
Expand Down
Loading
Loading