diff --git a/package.json b/package.json index 427eeef..c8dd0a4 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "emoji-log": "^1.0.2", "i18next": "^23.2.6", "i18next-browser-languagedetector": "^7.1.0", - "i18next-xhr-backend": "^3.2.2", "lodash": "^4.17.21", "webext-base-css": "^1.3.1", "webextension-polyfill": "0.10.0" diff --git a/source/Background/index.ts b/source/Background/index.ts index eb56ce8..60bd377 100644 --- a/source/Background/index.ts +++ b/source/Background/index.ts @@ -21,6 +21,7 @@ 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'; console.log('ZAP Service Worker 👋'); @@ -28,10 +29,11 @@ console.log('ZAP Service Worker 👋'); We check the storage on every page, so need to record which storage events we have reported to ZAP here so that we dont keep sending the same events. */ const reportedStorage = new Set(); -const zestScript = new ZestScript('recordedScript'); +const zestScript = new ZestScript(); /* A callback URL will only be available if the browser has been launched from ZAP, otherwise call the individual endpoints */ + function zapApiUrl(zapurl: string, action: string): string { if (zapurl.indexOf('/zapCallBackUrl/') > 0) { return zapurl; @@ -138,11 +140,29 @@ function reportCookies( return true; } -function handleMessage( +function sendZestScriptToZAP( + data: string, + zapkey: string, + zapurl: string +): void { + const body = `scriptJson=${encodeURIComponent( + data + )}&apikey=${encodeURIComponent(zapkey)}`; + console.log(`body = ${body}`); + fetch(zapApiUrl(zapurl, 'reportZestScript'), { + method: 'POST', + body, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }); +} + +async function handleMessage( request: MessageEvent, zapurl: string, zapkey: string -): boolean | ZestScriptMessage { +): Promise { if (request.type === 'zapDetails') { console.log('ZAP Service worker updating the ZAP details'); Browser.storage.sync.set({ @@ -197,22 +217,35 @@ function handleMessage( }, }); } else if (request.type === 'zestScript') { + const stmt = JSON.parse(request.data); + if (stmt.elementType === 'ZestClientElementSendKeys') { + console.log(stmt); + stmt.elementType = 'ZestClientElementClear'; + delete stmt.value; + const cleardata = zestScript.addStatement(JSON.stringify(stmt)); + sendZestScriptToZAP(cleardata, zapkey, zapurl); + } const data = zestScript.addStatement(request.data); - const body = `scriptJson=${encodeURIComponent( - data - )}&apikey=${encodeURIComponent(zapkey)}`; - console.log(`body = ${body}`); - fetch(zapApiUrl(zapurl, 'reportZestScript'), { - method: 'POST', - body, - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }); + sendZestScriptToZAP(data, zapkey, zapurl); } else if (request.type === 'saveZestScript') { return zestScript.getZestScript(); } else if (request.type === 'resetZestScript') { zestScript.reset(); + } else if (request.type === 'stopRecording') { + if (zestScript.getZestStatementCount() > 0) { + const {zapclosewindowhandle} = await Browser.storage.sync.get({ + zapclosewindowhandle: false, + }); + if (zapclosewindowhandle) { + const stmt = new ZestStatementWindowClose(0); + const data = zestScript.addStatement(stmt.toJSON()); + sendZestScriptToZAP(data, zapkey, zapurl); + } + } + } else if (request.type === 'setSaveScriptEnable') { + Browser.storage.sync.set({ + zapenablesavescript: zestScript.getZestStatementCount() > 0, + }); } return true; } @@ -226,7 +259,7 @@ async function onMessageHandler( zapurl: 'http://zap/', zapkey: 'not set', }); - const msg = handleMessage(message, items.zapurl, items.zapkey); + const msg = await handleMessage(message, items.zapurl, items.zapkey); if (!(typeof msg === 'boolean')) { val = msg; } diff --git a/source/ContentScript/index.ts b/source/ContentScript/index.ts index 58659b6..4de1a4f 100644 --- a/source/ContentScript/index.ts +++ b/source/ContentScript/index.ts @@ -24,16 +24,14 @@ import { ReportedStorage, ReportedEvent, } from '../types/ReportedModel'; -import { - initializationScript, - recordUserInteractions, - stopRecordingUserInteractions, -} from './userInteractions'; +import Recorder from './recorder'; const reportedObjects = new Set(); const reportedEvents: {[key: string]: ReportedEvent} = {}; +const recorder = new Recorder(); + function reportStorage( name: string, storage: Storage, @@ -226,18 +224,22 @@ function enableExtension(): void { reportPageLoaded(document, reportObject); } +function configureExtension(): void { + const localzapurl = localStorage.getItem('localzapurl'); + const localzapenable = localStorage.getItem('localzapenable') || true; + if (localzapurl) { + Browser.storage.sync.set({ + zapurl: localzapurl, + zapenable: localzapenable !== 'false', + }); + } +} + function injectScript(): Promise { return new Promise((resolve) => { - const localzapurl = localStorage.getItem('localzapurl'); - const localzapenable = localStorage.getItem('localzapenable') || true; - if (localzapurl) { - Browser.storage.sync.set({ - zapurl: localzapurl, - zapenable: localzapenable !== 'false', - }); - } + configureExtension(); withZapRecordingActive(() => { - recordUserInteractions(); + recorder.recordUserInteractions(); }); withZapEnableSetting(() => { enableExtension(); @@ -252,10 +254,11 @@ injectScript(); Browser.runtime.onMessage.addListener( (message: MessageEvent, _sender: Runtime.MessageSender) => { if (message.type === 'zapStartRecording') { - initializationScript(); - recordUserInteractions(); + configureExtension(); + recorder.initializationScript(); + recorder.recordUserInteractions(); } else if (message.type === 'zapStopRecording') { - stopRecordingUserInteractions(); + recorder.stopRecordingUserInteractions(); } } ); diff --git a/source/ContentScript/recorder.ts b/source/ContentScript/recorder.ts new file mode 100644 index 0000000..b3a796d --- /dev/null +++ b/source/ContentScript/recorder.ts @@ -0,0 +1,221 @@ +/* + * Zed Attack Proxy (ZAP) and its related source files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import debounce from 'lodash/debounce'; +import Browser from 'webextension-polyfill'; +import { + ZestStatement, + ZestStatementElementClick, + ZestStatementElementSendKeys, + ZestStatementLaunchBrowser, +} from '../types/zestScript/ZestStatement'; +import {getPath} from './util'; + +class Recorder { + previousDOMState: string; + + curLevel = -1; + + curFrame = 0; + + active = false; + + haveListenersBeenAdded = false; + + async sendZestScriptToZAP(zestStatement: ZestStatement): Promise { + return Browser.runtime.sendMessage({ + type: 'zestScript', + data: zestStatement.toJSON(), + }); + } + + handleFrameSwitches(level: number, frame: number): void { + if (this.curLevel === level && this.curFrame === frame) { + // do nothing + } else if (this.curLevel > level) { + while (this.curLevel > level) { + this.curLevel -= 1; + console.log( + 'Switched to level: ', + this.curLevel, + 'Frame:', + this.curFrame + ); + // switch to parent frame + } + this.curFrame = frame; + console.log( + 'Switched to level: ', + this.curLevel, + 'Frame:', + this.curFrame + ); + // switch to frame + } else { + this.curLevel += 1; + this.curFrame = frame; + console.log( + 'Switched to level: ', + this.curLevel, + 'Frame:', + this.curFrame + ); + // switch to frame number 'frame' + } + } + + handleClick( + params: {level: number; frame: number; element: Document}, + event: Event + ): void { + if (!this.active) return; + const {level, frame, element} = params; + this.handleFrameSwitches(level, frame); + console.log(event, 'clicked'); + const elementLocator = getPath(event.target as HTMLElement, element); + this.sendZestScriptToZAP(new ZestStatementElementClick(elementLocator)); + // click on target element + } + + handleScroll(params: {level: number; frame: number}, event: Event): void { + if (!this.active) return; + const {level, frame} = params; + this.handleFrameSwitches(level, frame); + console.log(event, 'scrolling.. '); + // scroll the nearest ancestor with scrolling ability + } + + handleMouseOver( + params: {level: number; frame: number; element: Document}, + event: Event + ): void { + if (!this.active) return; + const {level, frame, element} = params; + const currentDOMState = element.documentElement.outerHTML; + if (currentDOMState === this.previousDOMState) { + return; + } + this.previousDOMState = currentDOMState; + this.handleFrameSwitches(level, frame); + console.log(event, 'MouseOver'); + // send mouseover event + } + + handleChange( + params: {level: number; frame: number; element: Document}, + event: Event + ): void { + if (!this.active) return; + const {level, frame, element} = params; + this.handleFrameSwitches(level, frame); + console.log(event, 'change', (event.target as HTMLInputElement).value); + const elementLocator = getPath(event.target as HTMLElement, element); + this.sendZestScriptToZAP( + new ZestStatementElementSendKeys( + elementLocator, + (event.target as HTMLInputElement).value + ) + ); + } + + handleResize(): void { + if (!this.active) return; + const width = + window.innerWidth || + document.documentElement.clientWidth || + document.body.clientWidth; + const height = + window.innerHeight || + document.documentElement.clientHeight || + document.body.clientHeight; + // send window resize event + console.log('Window Resize : ', width, height); + } + + addListenersToDocument( + element: Document, + level: number, + frame: number + ): void { + element.addEventListener( + 'click', + this.handleClick.bind(this, {level, frame, element}) + ); + element.addEventListener( + 'scroll', + debounce(this.handleScroll.bind(this, {level, frame, element}), 1000) + ); + element.addEventListener( + 'mouseover', + this.handleMouseOver.bind(this, {level, frame, element}) + ); + element.addEventListener( + 'change', + this.handleChange.bind(this, {level, frame, element}) + ); + + // Add listeners to all the frames + const frames = element.querySelectorAll('frame, iframe'); + let i = 0; + frames.forEach((_frame) => { + const frameDocument = (_frame as HTMLIFrameElement | HTMLObjectElement) + .contentWindow?.document; + if (frameDocument != null) { + this.addListenersToDocument(frameDocument, level + 1, i); + i += 1; + } + }); + } + + getBrowserName(): string { + let browserName: string; + const {userAgent} = navigator; + if (userAgent.includes('Chrome')) { + browserName = 'chrome'; + } else { + browserName = 'firefox'; + } + return browserName; + } + + initializationScript(): void { + // send window resize event to ensure same size + const browserType = this.getBrowserName(); + const url = window.location.href; + this.sendZestScriptToZAP(new ZestStatementLaunchBrowser(browserType, url)); + this.handleResize(); + } + + recordUserInteractions(): void { + console.log('user interactions'); + this.active = true; + this.previousDOMState = document.documentElement.outerHTML; + if (this.haveListenersBeenAdded) return; + this.haveListenersBeenAdded = true; + window.addEventListener('resize', debounce(this.handleResize, 100)); + this.addListenersToDocument(document, -1, 0); + } + + stopRecordingUserInteractions(): void { + console.log('Stopping Recording User Interactions ...'); + this.active = false; + } +} + +export default Recorder; diff --git a/source/ContentScript/userInteractions.ts b/source/ContentScript/userInteractions.ts deleted file mode 100644 index 1f9e49f..0000000 --- a/source/ContentScript/userInteractions.ts +++ /dev/null @@ -1,205 +0,0 @@ -/* - * Zed Attack Proxy (ZAP) and its related source files. - * - * ZAP is an HTTP/HTTPS proxy for assessing web application security. - * - * Copyright 2023 The ZAP Development Team - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -import debounce from 'lodash/debounce'; -import Browser from 'webextension-polyfill'; -import { - ZestStatement, - ZestStatementElementClick, - ZestStatementElementSendKeys, - ZestStatementLaunchBrowser, -} from '../types/zestScript/ZestStatement'; -import {getPath} from './util'; - -let previousDOMState: string; -let curLevel = -1; -let curFrame = 0; -let active = true; - -async function sendZestScriptToZAP( - zestStatement: ZestStatement -): Promise { - return Browser.runtime.sendMessage({ - type: 'zestScript', - data: zestStatement.toJSON(), - }); -} - -function handleFrameSwitches(level: number, frame: number): void { - if (curLevel === level && curFrame === frame) { - // do nothing - } else if (curLevel > level) { - while (curLevel > level) { - curLevel -= 1; - console.log('Switched to level: ', curLevel, 'Frame:', curFrame); - // switch to parent frame - } - curFrame = frame; - console.log('Switched to level: ', curLevel, 'Frame:', curFrame); - // switch to frame - } else { - curLevel += 1; - curFrame = frame; - console.log('Switched to level: ', curLevel, 'Frame:', curFrame); - // switch to frame number 'frame' - } -} - -function handleClick( - this: {level: number; frame: number; element: Document}, - event: Event -): void { - if (!active) return; - const {level, frame, element} = this; - handleFrameSwitches(level, frame); - console.log(event, 'clicked'); - const elementLocator = getPath(event.target as HTMLElement, element); - sendZestScriptToZAP(new ZestStatementElementClick(elementLocator)); - // click on target element -} - -function handleScroll( - this: {level: number; frame: number}, - event: Event -): void { - if (!active) return; - const {level, frame} = this; - handleFrameSwitches(level, frame); - console.log(event, 'scrolling.. '); - // scroll the nearest ancestor with scrolling ability -} - -function handleMouseOver( - this: {level: number; frame: number; element: Document}, - event: Event -): void { - if (!active) return; - const {level, frame, element} = this; - const currentDOMState = element.documentElement.outerHTML; - if (currentDOMState === previousDOMState) { - return; - } - previousDOMState = currentDOMState; - handleFrameSwitches(level, frame); - console.log(event, 'MouseOver'); - // send mouseover event -} - -function handleChange( - this: {level: number; frame: number; element: Document}, - event: Event -): void { - if (!active) return; - const {level, frame, element} = this; - handleFrameSwitches(level, frame); - console.log(event, 'change', (event.target as HTMLInputElement).value); - const elementLocator = getPath(event.target as HTMLElement, element); - sendZestScriptToZAP( - new ZestStatementElementSendKeys( - elementLocator, - (event.target as HTMLInputElement).value - ) - ); - // send keys to the element -} - -function handleResize(): void { - if (!active) return; - const width = - window.innerWidth || - document.documentElement.clientWidth || - document.body.clientWidth; - const height = - window.innerHeight || - document.documentElement.clientHeight || - document.body.clientHeight; - // send window resize event - console.log('Window Resize : ', width, height); -} - -function addListenersToDocument( - element: Document, - level: number, - frame: number -): void { - element.addEventListener('click', handleClick.bind({level, frame, element})); - element.addEventListener( - 'scroll', - debounce(handleScroll.bind({level, frame, element}), 1000) - ); - element.addEventListener( - 'mouseover', - handleMouseOver.bind({level, frame, element}) - ); - element.addEventListener( - 'change', - handleChange.bind({level, frame, element}) - ); - - // Add listeners to all the frames - const frames = element.querySelectorAll('frame, iframe'); - let i = 0; - frames.forEach((_frame) => { - const frameDocument = (_frame as HTMLIFrameElement | HTMLObjectElement) - .contentWindow?.document; - if (frameDocument != null) { - addListenersToDocument(frameDocument, level + 1, i); - i += 1; - } - }); -} - -function getBrowserName(): string { - let browserName: string; - const {userAgent} = navigator; - if (userAgent.includes('Chrome')) { - browserName = 'chrome'; - } else { - browserName = 'firefox'; - } - return browserName; -} - -function initializationScript(): void { - // send window resize event to ensure same size - const browserType = getBrowserName(); - const url = window.location.href; - sendZestScriptToZAP(new ZestStatementLaunchBrowser(browserType, url)); - handleResize(); - // TODO: goto URL specified -} - -function recordUserInteractions(): void { - console.log('user interactions'); - active = true; - previousDOMState = document.documentElement.outerHTML; - window.addEventListener('resize', debounce(handleResize, 100)); - addListenersToDocument(document, -1, 0); -} - -function stopRecordingUserInteractions(): void { - console.log('Stopping Recording User Interactions ...'); - active = false; -} - -export { - recordUserInteractions, - stopRecordingUserInteractions, - initializationScript, -}; diff --git a/source/Options/index.tsx b/source/Options/index.tsx index e91fb04..e5dd7da 100644 --- a/source/Options/index.tsx +++ b/source/Options/index.tsx @@ -24,7 +24,6 @@ console.log('Options loading'); const ZAP_URL = 'zapurl'; const ZAP_KEY = 'zapkey'; const ZAP_ENABLE = 'zapenable'; -const ZAP_RECORDING_ACTIVE = 'zaprecordingactive'; // Saves options to chrome.storage function saveOptions(): void { @@ -33,14 +32,14 @@ function saveOptions(): void { const zapkey = (document.getElementById(ZAP_KEY) as HTMLInputElement).value; const zapenable = (document.getElementById(ZAP_ENABLE) as HTMLInputElement) .checked; - const zaprecordingactive = ( - document.getElementById(ZAP_RECORDING_ACTIVE) as HTMLInputElement + const zapclosewindowhandle = ( + document.getElementById('window-close-input') as HTMLInputElement ).checked; Browser.storage.sync.set({ zapurl, zapkey, zapenable, - zaprecordingactive, + zapclosewindowhandle, }); } @@ -54,6 +53,7 @@ function restoreOptions(): void { zapkey: 'not set', zapenable: true, zaprecordingactive: false, + zapclosewindowhandle: true, }) .then((items) => { (document.getElementById(ZAP_URL) as HTMLInputElement).value = @@ -63,8 +63,8 @@ function restoreOptions(): void { (document.getElementById(ZAP_ENABLE) as HTMLInputElement).checked = items.zapenable; ( - document.getElementById(ZAP_RECORDING_ACTIVE) as HTMLInputElement - ).checked = items.zaprecordingactive; + document.getElementById('window-close-input') as HTMLInputElement + ).checked = items.zapclosewindowhandle; }); } document.addEventListener('DOMContentLoaded', restoreOptions); diff --git a/source/Popup/i18n.tsx b/source/Popup/i18n.tsx index 6d27a7d..4050812 100644 --- a/source/Popup/i18n.tsx +++ b/source/Popup/i18n.tsx @@ -1,19 +1,42 @@ +/* + * Zed Attack Proxy (ZAP) and its related source files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ import i18n from 'i18next'; import LanguageDetector from 'i18next-browser-languagedetector'; -import XHRBackend from 'i18next-xhr-backend'; -i18n - .use(XHRBackend) - .use(LanguageDetector) - .init({ - backend: { - loadPath: '../assets/locales/{{lng}}/{{ns}}.json', +i18n.use(LanguageDetector).init({ + resources: { + en: { + translation: { + start: 'Start Recording', + stop: 'Stop Recording', + download: 'Download Script', + options: 'Options', + }, }, - fallbackLng: 'en', - debug: false, - interpolation: { - escapeValue: false, - }, - }); + }, + defaultNS: 'translation', + fallbackLng: 'en', + debug: false, + interpolation: { + escapeValue: false, + }, +}); export default i18n; diff --git a/source/Popup/index.tsx b/source/Popup/index.tsx index 98bcde8..0150dea 100644 --- a/source/Popup/index.tsx +++ b/source/Popup/index.tsx @@ -21,33 +21,83 @@ import Browser from 'webextension-polyfill'; import './styles.scss'; import i18n from './i18n'; -let recordingActive = false; -const RECORD = i18n.t('Record'); -const STOP = i18n.t('Stop'); +const STOP = i18n.t('stop'); +const START = i18n.t('start'); +const OPTIONS = i18n.t('options'); +const DOWNLOAD = i18n.t('download'); + +const play = document.querySelector('.play'); +const pause = document.querySelector('.pause'); +const wave1 = document.querySelector('.record__back-1'); +const wave2 = document.querySelector('.record__back-2'); +const done = document.querySelector('.done'); +const optionsIcon = document.querySelector('.settings') as HTMLImageElement; +const downloadIcon = document.querySelector('.download') as HTMLImageElement; -function sendMessageToContentScript(message: string): void { +const recordButton = document.getElementById('record-btn'); +const configureButton = document.getElementById('configure-btn'); +const saveScript = document.getElementById('save-script'); +const scriptNameInput = document.getElementById( + 'script-name-input' +) as HTMLInputElement; +const saveScriptButton = document.getElementById( + 'save-script' +) as HTMLButtonElement; + +function sendMessageToContentScript(message: string, data = ''): void { Browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { const activeTab = tabs[0]; if (activeTab?.id) { - Browser.tabs.sendMessage(activeTab.id, {type: message}); + Browser.tabs.sendMessage(activeTab.id, {type: message, data}); } }); } -function restoreState(): void { - console.log('Restore state'); +function stoppedAnimation(): void { + pause?.classList.add('visibility'); + play?.classList.add('visibility'); + recordButton?.classList.add('shadow'); + wave1?.classList.add('paused'); + wave2?.classList.add('paused'); + (play as HTMLImageElement).title = START; +} + +function startedAnimation(): void { + pause?.classList.remove('visibility'); + play?.classList.remove('visibility'); + recordButton?.classList.remove('shadow'); + wave1?.classList.remove('paused'); + wave2?.classList.remove('paused'); + (play as HTMLImageElement).title = STOP; +} +async function restoreState(): Promise { + console.log('Restore state'); + await Browser.runtime.sendMessage({type: 'setSaveScriptEnable'}); + optionsIcon.title = OPTIONS; + downloadIcon.title = DOWNLOAD; Browser.storage.sync .get({ zaprecordingactive: false, + zapscriptname: '', + zapenablesavescript: false, }) .then((items) => { - recordingActive = items.zaprecordingactive; - if (recordingActive) { - const recordButton = document.getElementById( - 'record-btn' - ) as HTMLButtonElement; - recordButton.textContent = STOP; + if (items.zaprecordingactive) { + startedAnimation(); + } else { + stoppedAnimation(); + } + scriptNameInput.value = items.zapscriptname; + if (items.zapclosewindowhandle) { + done?.classList.remove('invisible'); + } else { + done?.classList.add('invisible'); + } + if (!items.zapenablesavescript) { + saveScriptButton.classList.add('disabled'); + } else { + saveScriptButton.classList.remove('disabled'); } }); } @@ -60,39 +110,34 @@ function closePopup(): void { function stopRecording(): void { console.log('Recording stopped ...'); + stoppedAnimation(); sendMessageToContentScript('zapStopRecording'); + Browser.runtime.sendMessage({type: 'stopRecording'}); Browser.storage.sync.set({ zaprecordingactive: false, }); - const recordButton = document.getElementById( - 'record-btn' - ) as HTMLButtonElement; - recordButton.textContent = RECORD; - recordingActive = false; } function startRecording(): void { - console.log('Recording started ...'); + startedAnimation(); sendMessageToContentScript('zapStartRecording'); Browser.runtime.sendMessage({type: 'resetZestScript'}); - Browser.storage.sync.set({ zaprecordingactive: true, }); - const recordButton = document.getElementById( - 'record-btn' - ) as HTMLButtonElement; - recordButton.textContent = STOP; - recordingActive = true; } -function toggleRecording(): void { - if (recordingActive) { - stopRecording(); - } else { - closePopup(); - startRecording(); - } +function toggleRecording(e: Event): void { + e.preventDefault(); + Browser.storage.sync.get({zaprecordingactive: false}).then((items) => { + if (items.zaprecordingactive) { + stopRecording(); + console.log('active'); + } else { + startRecording(); + closePopup(); + } + }); } function openOptionsPage(): void { @@ -103,12 +148,16 @@ function openOptionsPage(): void { } function downloadZestScript(zestScriptJSON: string, title: string): void { + if (title === '') { + scriptNameInput?.focus(); + return; + } const blob = new Blob([zestScriptJSON], {type: 'application/json'}); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; - link.download = `${title}.zst`; + link.download = title + (title.slice(-4) === '.zst' ? '' : '.zst'); link.style.display = 'none'; document.body.appendChild(link); @@ -116,7 +165,10 @@ function downloadZestScript(zestScriptJSON: string, title: string): void { document.body.removeChild(link); URL.revokeObjectURL(url); - + Browser.runtime.sendMessage({type: 'resetZestScript'}); + Browser.storage.sync.set({ + zaprecordingactive: false, + }); closePopup(); } @@ -126,9 +178,13 @@ function handleSaveScript(): void { }); } -const recordButton = document.getElementById('record-btn'); -const configureButton = document.getElementById('configure-btn'); -const saveScript = document.getElementById('save-script'); +function handleScriptNameChange(e: Event): void { + const {value} = e.target as HTMLInputElement; + Browser.storage.sync.set({ + zapscriptname: value, + }); + sendMessageToContentScript('updateTitle', value); +} document.addEventListener('DOMContentLoaded', restoreState); document.addEventListener('load', restoreState); @@ -136,3 +192,4 @@ document.addEventListener('load', restoreState); recordButton?.addEventListener('click', toggleRecording); configureButton?.addEventListener('click', openOptionsPage); saveScript?.addEventListener('click', handleSaveScript); +scriptNameInput?.addEventListener('input', handleScriptNameChange); diff --git a/source/Popup/styles.scss b/source/Popup/styles.scss index 0855ac3..bc0e677 100644 --- a/source/Popup/styles.scss +++ b/source/Popup/styles.scss @@ -1,8 +1,7 @@ @import "../styles/fonts"; -@import "../styles/reset"; @import "../styles/variables"; - @import "~webext-base-css/webext-base.css"; + @font-face { font-family: 'Roboto'; font-style: normal; @@ -19,45 +18,268 @@ body { font-family: "Roboto", sans-serif; + margin: 0px; +} + +:root { + --primary-light: #F0F5FF; /* Light blue */ + --primary: #2979FF; /* Vivid blue */ + --primary-dark: #ff1500; /* Dark blue */ + + --white: #FFFFFF; /* Pure white */ + --greyLight-1: #F2F2F2; /* Light grey */ + --greyLight-2: #D9D9D9; /* Medium grey */ + --greyLight-3: #B0B0B0; /* Dark grey */ + --greyDark: #666666; /* Deeper dark grey */ + --text-color: #333333; /* Dark text color */ +} + + + +$shadow: .3rem .3rem .6rem var(--greyLight-2), +-.2rem -.2rem .5rem var(--white); +$inner-shadow: inset .2rem .2rem .5rem var(--greyLight-2), +inset -.2rem -.2rem .5rem var(--white); + +*, *::before, *::after { margin: 0; padding: 0; + box-sizing: inherit; +} + +html { + box-sizing: border-box; + font-size:10px !important; + overflow-y: scroll; + background: var(--greyLight-1); + margin: 0px; +} + +.container { + min-height: 100vh; display: flex; justify-content: center; align-items: center; - height: 100vh; - background-color: #f2f2f2; + background: var(--greyLight-1); } -.container { +.components { + width: 30rem; + height: 35rem; + padding: 2rem; + display: grid; + grid-template-columns: 0.5rem 0.5rem 0.5rem; + grid-template-rows: repeat(autofit, min-content); + grid-column-gap: 5rem; + grid-row-gap: 2.5rem; + align-items: center; + } + +.title{ + grid-column: 1/6; + grid-row: 5; text-align: center; - background-color: #fff; - border-radius: 8px; - padding: 40px; - box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1); + color: var(--text-color); + display: flex; + align-items: center; + justify-content: center; } -.title { - font-size: 24px; - font-weight: 700; - margin-bottom: 20px; +.zapicon { + width: 4rem; + height: 4rem; + margin-right: 1rem; } -.button { - display: inline-block; - padding: 12px 24px; - margin: 2px; - font-size: 16px; - font-weight: 700; - text-align: center; - text-decoration: none; - border-radius: 4px; - background-color: #007bff; - color: #fff; - border: none; +.zaptitle { + font-size: 2.4rem; + font-weight: bold; +} + +/* PLAY BUTTON */ +.record { + grid-column: 1 / 6; + grid-row: 1 / 4; + width: 9rem; + height: 100%; + justify-self: center; + border-radius: 1rem; + display: grid; + grid-template-rows: 1fr; + justify-items: center; + align-items: center; + + &__btn { + grid-row: 1 / 2; + grid-column: 1 / 2; + width: 6rem; + height: 6rem; + display: flex; + margin: .6rem; + justify-content: center; + align-items: center; + border-radius: 50%; + font-size: 3.2rem; + color: var(--primary); + z-index: 300; + background: var(--greyLight-1); + box-shadow: $shadow; + cursor: pointer; + position: relative; + &.shadow {box-shadow: $inner-shadow;} + + .play { + position: absolute; + opacity: 0; + transition: all .2s linear; + &.visibility { + opacity: 1; + } + } + .pause { + position: absolute; + transition: all .2s linear; + &.visibility { + opacity: 0; + } + } + } + + &__back-1, &__back-2 { + grid-row: 1 / 2; + grid-column: 1 / 2; + width: 6rem; + height: 6rem; + border-radius: 50%; + filter: blur(1px); + z-index: 100; + } + + &__back-1 { + box-shadow: .4rem .4rem .8rem var(--greyLight-2), + -.4rem -.4rem .8rem var(--white); + background: linear-gradient(to bottom right, var(--greyLight-2) 0%, var(--white) 100%); + animation: waves 4s linear infinite; + + &.paused { + animation-play-state: paused; + } + } + + &__back-2 { + box-shadow: .4rem .4rem .8rem var(--greyLight-2), + -.4rem -.4rem .8rem var(--white); + animation: waves 4s linear 2s infinite; + + &.paused { + animation-play-state: paused; + } + } +} + +/* FORM */ +.form { + grid-column: 1 / 6; + grid-row: 4 / 5 ; + align-self: center; + display: flex; + justify-content:space-between; + color: var(--text-color); + + &__input { + width: 20.4rem; + height: 4rem; + border: none; + border-radius: 1rem; + font-size: 1.4rem; + padding-left: 1.4rem; + box-shadow: $inner-shadow; + background: none; + font-family: inherit; + + &::placeholder { color: var(--greyLight-3); } + &:focus { outline: none; box-shadow: $shadow; } + &:focus-visible { + outline: var(--text-color); + box-shadow: $shadow; + } + } +} + +/* ICONS */ +.setting-icon { + grid-column: 1; + grid-row: 1 ; + &__settings { + width: 4rem; + height: 4rem; + border-radius: 50%; + box-shadow: $shadow; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; + cursor: pointer; + color: var(--greyDark); + transition: all .5s ease; + + &:active { + box-shadow: $inner-shadow; + color: var(--primary); + } + &:hover {color: var(--primary);} + } + +} + + +/* ICONS */ + +.download-icon { + width: 4rem; + height: 4rem; + border-radius: 50%; + box-shadow: $shadow; + display: flex; + justify-content: center; + align-items: center; + font-size: 2rem; cursor: pointer; - transition: background-color 0.3s ease; + color: var(--greyDark); + transition: all .5s ease; + + &:active { + box-shadow: $inner-shadow; + color: var(--primary); + } + &:hover {color: var(--primary);} + + &.disabled { + pointer-events: none; + opacity: 0.5; + } +} + +@keyframes waves { + 0% { + transform: scale(1); + opacity: 1; + } + + 50% { + opacity: 1; + } + + 100% { + transform: scale(2); + opacity: 0; + } +} + +.pause, .play{ + width: 3rem; } -.button:hover { - background-color: #0056b3; +.download, .settings , .done{ + width: 2rem; } \ No newline at end of file diff --git a/source/assets/icons/done.svg b/source/assets/icons/done.svg new file mode 100644 index 0000000..5953fa3 --- /dev/null +++ b/source/assets/icons/done.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/icons/download.svg b/source/assets/icons/download.svg new file mode 100644 index 0000000..dc8f768 --- /dev/null +++ b/source/assets/icons/download.svg @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/source/assets/icons/pause.svg b/source/assets/icons/pause.svg new file mode 100644 index 0000000..b187db4 --- /dev/null +++ b/source/assets/icons/pause.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/icons/play.svg b/source/assets/icons/play.svg new file mode 100644 index 0000000..dd967d8 --- /dev/null +++ b/source/assets/icons/play.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/icons/radio-button-on-outline.svg b/source/assets/icons/radio-button-on-outline.svg new file mode 100644 index 0000000..cce1273 --- /dev/null +++ b/source/assets/icons/radio-button-on-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/icons/settings.svg b/source/assets/icons/settings.svg new file mode 100644 index 0000000..7e15335 --- /dev/null +++ b/source/assets/icons/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/icons/stop-circle-outline.svg b/source/assets/icons/stop-circle-outline.svg new file mode 100644 index 0000000..614dfc3 --- /dev/null +++ b/source/assets/icons/stop-circle-outline.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/source/assets/locales/en/translation.json b/source/assets/locales/en/translation.json deleted file mode 100644 index 7229ca8..0000000 --- a/source/assets/locales/en/translation.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "Record": "Record", - "Stop": "Stop" -} \ No newline at end of file diff --git a/source/types/zestScript/ZestScript.ts b/source/types/zestScript/ZestScript.ts index 6703fde..f769dfe 100644 --- a/source/types/zestScript/ZestScript.ts +++ b/source/types/zestScript/ZestScript.ts @@ -1,14 +1,35 @@ +/* + * Zed Attack Proxy (ZAP) and its related source files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import Browser from 'webextension-polyfill'; + interface ZestScriptMessage { script: string; title: string; } class ZestScript { - zestStatements: string[] = []; + private zestStatements: string[] = []; - curIndex = 1; + private curIndex = 1; - title: string; + private title: string; constructor(title = '') { this.title = title; @@ -31,13 +52,21 @@ class ZestScript { this.curIndex = 1; } + getZestStatementCount(): number { + return this.zestStatements.length; + } + + getTitle(): string { + return this.title; + } + toJSON(): string { return JSON.stringify( { about: 'This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/', zestVersion: '0.3', - title: 'recordedScript', + title: this.title, description: '', prefix: '', type: 'StandAlone', @@ -60,8 +89,13 @@ class ZestScript { ); } - getZestScript(): ZestScriptMessage { - return {script: this.toJSON(), title: this.title}; + getZestScript(): Promise { + return new Promise((resolve) => { + Browser.storage.sync.get({zapscriptname: this.title}).then((items) => { + this.title = items.zapscriptname; + resolve({script: this.toJSON(), title: this.title}); + }); + }); } } diff --git a/source/types/zestScript/ZestStatement.ts b/source/types/zestScript/ZestStatement.ts index 8b79616..c4b58db 100644 --- a/source/types/zestScript/ZestStatement.ts +++ b/source/types/zestScript/ZestStatement.ts @@ -1,3 +1,22 @@ +/* + * Zed Attack Proxy (ZAP) and its related source files. + * + * ZAP is an HTTP/HTTPS proxy for assessing web application security. + * + * Copyright 2023 The ZAP Development Team + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ class ElementLocator { type: string; @@ -114,6 +133,45 @@ class ZestStatementElementSendKeys extends ZestStatementElement { } } +class ZestStatementElementClear extends ZestStatementElement { + constructor(elementLocator: ElementLocator, windowHandle = 'windowHandle1') { + super('ZestClientElementClear', elementLocator); + this.windowHandle = windowHandle; + } + + toJSON(): string { + return JSON.stringify({ + windowHandle: this.windowHandle, + ...this.elementLocator.toJSON(), + index: this.index, + enabled: true, + elementType: this.elementType, + }); + } +} + +class ZestStatementWindowClose extends ZestStatement { + sleepInSeconds: number; + + windowHandle: string; + + constructor(sleepInSeconds: number, windowHandle = 'windowHandle1') { + super('ZestClientWindowClose'); + this.sleepInSeconds = sleepInSeconds; + this.windowHandle = windowHandle; + } + + toJSON(): string { + return JSON.stringify({ + windowHandle: this.windowHandle, + index: this.index, + sleepInSeconds: this.sleepInSeconds, + enabled: true, + elementType: this.elementType, + }); + } +} + class ZestStatementSwichToFrame extends ZestStatement { frameIndex: number; @@ -150,4 +208,6 @@ export { ZestStatementElementClick, ZestStatementSwichToFrame, ZestStatementElementSendKeys, + ZestStatementElementClear, + ZestStatementWindowClose, }; diff --git a/test/ContentScript/integrationTests.test.ts b/test/ContentScript/integrationTests.test.ts index 418c8f7..0f741f3 100644 --- a/test/ContentScript/integrationTests.test.ts +++ b/test/ContentScript/integrationTests.test.ts @@ -113,14 +113,15 @@ function integrationTests( await page.goto( `http://localhost:${_HTTPPORT}/webpages/interactions.html` ); + await page; await page.fill('#input-1', 'testinput'); - await page.fill('#input-2', '2023-06-15'); + await page.click('#click'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); await page.close(); // Then const expectedData = - '["{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"value\\":\\"testinput\\",\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":1,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSendKeys\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"value\\":\\"2023-06-15\\",\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-2\\",\\"index\\":2,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSendKeys\\"}\\",\\"apikey\\":\\"not set\\"}}"]'; + '["{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":1,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementClear\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"value\\":\\"testinput\\",\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":2,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSendKeys\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"click\\",\\"index\\":3,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementClick\\"}\\",\\"apikey\\":\\"not set\\"}}"]'; expect(JSON.stringify(Array.from(actualData))).toBe(expectedData); }); @@ -135,7 +136,7 @@ function integrationTests( `http://localhost:${_HTTPPORT}/webpages/interactions.html` ); await page.fill('#input-1', 'testinput'); - await page.fill('#input-2', '2023-06-15'); + await page.click('#click'); await page.waitForLoadState('networkidle'); await page.waitForTimeout(1000); await page.close(); @@ -150,6 +151,12 @@ function integrationTests( await driver.setEnable(false); const page = await context.newPage(); await page.goto(await driver.getPopupURL()); + await page.fill('#script-name-input', 'recordedScript'); + await page.goto( + `http://localhost:${_HTTPPORT}/webpages/interactions.html` + ); + await page.click('#click'); + await page.goto(await driver.getPopupURL()); let actualOutcome = ''; page.on('download', async (download) => { actualOutcome = download.suggestedFilename(); @@ -163,6 +170,54 @@ function integrationTests( // Then expect(actualOutcome).toBe('recordedScript.zst'); }); + + test('Should send window handle close script when enabled', async () => { + server = getFakeZapServer(actualData, _JSONPORT); + const context = await driver.getContext(_JSONPORT, true); + await driver.setEnable(false); + const page = await context.newPage(); + await page.goto(await driver.getOptionsURL()); + await page.goto( + `http://localhost:${_HTTPPORT}/webpages/interactions.html` + ); + await page.fill('#input-1', 'testinput'); + await page.click('#click'); + await page.goto(await driver.getPopupURL()); + await page.click('#record-btn'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + await page.close(); + // Then + const expectedData = + '["{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":1,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementClear\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"value\\":\\"testinput\\",\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"input-1\\",\\"index\\":2,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementSendKeys\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"type\\":\\"id\\",\\"element\\":\\"click\\",\\"index\\":3,\\"enabled\\":true,\\"elementType\\":\\"ZestClientElementClick\\"}\\",\\"apikey\\":\\"not set\\"}}","{\\"action\\":{\\"action\\":\\"reportZestScript\\"},\\"body\\":{\\"scriptJson\\":\\"{\\"windowHandle\\":\\"windowHandle1\\",\\"index\\":4,\\"sleepInSeconds\\":0,\\"enabled\\":true,\\"elementType\\":\\"ZestClientWindowClose\\"}\\",\\"apikey\\":\\"not set\\"}}"]'; + expect(JSON.stringify(Array.from(actualData))).toBe(expectedData); + }); + + test('Should configure downloaded script name', async () => { + // Given + server = getFakeZapServer(actualData, _JSONPORT); + const context = await driver.getContext(_JSONPORT, true); + await driver.setEnable(false); + const page = await context.newPage(); + await page.goto( + `http://localhost:${_HTTPPORT}/webpages/interactions.html` + ); + await page.click('#click'); + await page.goto(await driver.getPopupURL()); + await page.fill('#script-name-input', 'test-name'); + let actualOutcome = ''; + page.on('download', async (download) => { + actualOutcome = download.suggestedFilename(); + await download.delete(); + }); + // When + await page.click('#save-script'); + await page.waitForLoadState('networkidle'); + await page.waitForTimeout(1000); + await page.close(); + // Then + expect(actualOutcome).toBe('test-name.zst'); + }); } } diff --git a/test/ContentScript/unitTests.test.ts b/test/ContentScript/unitTests.test.ts index 1bae28d..dfa1e4f 100644 --- a/test/ContentScript/unitTests.test.ts +++ b/test/ContentScript/unitTests.test.ts @@ -282,7 +282,7 @@ test('Should Disable The Extension', async () => { }); test('should generate valid script', () => { - const script = new ZestScript(); + const script = new ZestScript('recordedScript'); const expectedOutcome = `{ "about": "This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/", "zestVersion": "0.3", @@ -329,7 +329,7 @@ test('should generate valid send keys statement', () => { }); test('should add zest statement to zest script', () => { - const script = new ZestScript(); + const script = new ZestScript('recordedScript'); const elementLocator = new ElementLocator('id', 'test'); const zestStatementElementClick = new ZestStatementElementClick( elementLocator @@ -367,7 +367,7 @@ test('should add zest statement to zest script', () => { }); test('should reset zest script', () => { - const script = new ZestScript(); + const script = new ZestScript('recordedScript'); const elementLocator = new ElementLocator('id', 'test'); const zestStatementElementClick = new ZestStatementElementClick( elementLocator diff --git a/views/options.html b/views/options.html index 7eea58e..f4c23c1 100644 --- a/views/options.html +++ b/views/options.html @@ -27,8 +27,8 @@

ZAP Browser Extension

- - + + diff --git a/views/popup.html b/views/popup.html index 807b1b9..4596b72 100644 --- a/views/popup.html +++ b/views/popup.html @@ -1,20 +1,44 @@ - - - - - ZAP Browser Extension + + + + ZAP Browser Extension -
-

ZAP

-
- - - -
-
+ +
+
+

+ + ZAP +

+ +
+ + + + + + +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+
+
+ \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 5155e7e..6b626ad 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1019,7 +1019,7 @@ dependencies: regenerator-runtime "^0.13.11" -"@babel/runtime@^7.19.4", "@babel/runtime@^7.22.5", "@babel/runtime@^7.5.5": +"@babel/runtime@^7.19.4", "@babel/runtime@^7.22.5": version "7.22.5" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec" integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA== @@ -4721,13 +4721,6 @@ i18next-browser-languagedetector@^7.1.0: dependencies: "@babel/runtime" "^7.19.4" -i18next-xhr-backend@^3.2.2: - version "3.2.2" - resolved "https://registry.yarnpkg.com/i18next-xhr-backend/-/i18next-xhr-backend-3.2.2.tgz#769124441461b085291f539d91864e3691199178" - integrity sha512-OtRf2Vo3IqAxsttQbpjYnmMML12IMB5e0fc5B7qKJFLScitYaXa1OhMX0n0X/3vrfFlpHL9Ro/H+ps4Ej2j7QQ== - dependencies: - "@babel/runtime" "^7.5.5" - i18next@^23.2.6: version "23.2.6" resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.2.6.tgz#70c09517c301f206615acd6fc35b4a2570629300"