├── .babelrc ├── .eslintignore ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ └── ci.yaml ├── .gitignore ├── CHANGELOG.md ├── CHANGELOG.rec.md ├── LICENSE ├── README.md ├── RELEASING.md ├── __mocks__ ├── tsconfig.json └── webextension-polyfill.ts ├── docs ├── README.md ├── screenshots │ ├── zap-chrome-options-smaller.png │ ├── zap-chrome-options.png │ ├── zap-client-windows-smaller.png │ ├── zap-client-windows.png │ ├── zap-firefox-options.png │ └── zap-record.png └── zap128x128.png ├── package.json ├── source ├── Background │ └── index.ts ├── ContentScript │ ├── index.ts │ ├── recorder.ts │ └── util.ts ├── Options │ ├── index.tsx │ └── styles.scss ├── Popup │ ├── i18n.tsx │ ├── index.tsx │ └── styles.scss ├── assets │ ├── fonts │ │ ├── Roboto-Bold.ttf │ │ └── Roboto-Regular.ttf │ └── icons │ │ ├── ZAP_by_Checkmarx_logo.png │ │ ├── done.svg │ │ ├── download.svg │ │ ├── help.svg │ │ ├── pause.svg │ │ ├── play.svg │ │ ├── radio-button-on-outline.svg │ │ ├── settings.svg │ │ ├── stop-circle-outline.svg │ │ ├── zap128x128.png │ │ ├── zap16x16.png │ │ ├── zap256x256.png │ │ ├── zap32x32.png │ │ ├── zap48x48.png │ │ └── zap512x512.png ├── manifest.json ├── manifest.rec.json ├── styles │ ├── _fonts.scss │ ├── _reset.scss │ └── _variables.scss ├── types │ ├── ReportedModel.ts │ └── zestScript │ │ ├── ZestScript.ts │ │ └── ZestStatement.ts └── utils │ └── constants.ts ├── test ├── Background │ ├── tsconfig.json │ └── unitTests.test.ts ├── ContentScript │ ├── constants.ts │ ├── integrationTests.test.ts │ ├── tsconfig.json │ ├── unitTests.test.ts │ ├── utils.ts │ └── webpages │ │ ├── integrationTest.html │ │ ├── interactions.html │ │ ├── linkedpage1.html │ │ ├── linkedpage2.html │ │ ├── linkedpage3.html │ │ ├── localStorage.html │ │ ├── localStorageDelay.html │ │ ├── sessionStorage.html │ │ ├── sessionStorageDelay.html │ │ └── testFrame.html └── drivers │ ├── ChromeDriver.ts │ └── FirefoxDriver.ts ├── tsconfig.json ├── views ├── basic.scss ├── help.html ├── options.html └── popup.html ├── webpack.config.js ├── webpack.config.rec.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | [ 4 | // Latest stable ECMAScript features 5 | "@babel/preset-env", 6 | { 7 | "useBuiltIns": false, 8 | // Do not transform modules to CJS 9 | "modules": false, 10 | "targets": { 11 | "chrome": "49", 12 | "firefox": "52", 13 | "opera": "36", 14 | "edge": "79" 15 | } 16 | } 17 | ], 18 | "@babel/typescript", 19 | "@babel/react" 20 | ], 21 | "plugins": [ 22 | ["@babel/plugin-proposal-class-properties"], 23 | ["@babel/plugin-transform-destructuring", { 24 | "useBuiltIns": true 25 | }], 26 | ["@babel/plugin-proposal-object-rest-spread", { 27 | "useBuiltIns": true 28 | }], 29 | [ 30 | // Polyfills the runtime needed for async/await and generators 31 | "@babel/plugin-transform-runtime", 32 | { 33 | "helpers": false, 34 | "regenerator": true 35 | } 36 | ] 37 | ] 38 | } -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | extension/ 4 | .yarn/ 5 | .pnp.js -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "@abhijithvijayan/eslint-config/typescript", 4 | "@abhijithvijayan/eslint-config/node", 5 | "@abhijithvijayan/eslint-config/react" 6 | ], 7 | "parserOptions": { 8 | "project": [ 9 | "./tsconfig.json", 10 | "./test/ContentScript/tsconfig.json", 11 | "./test/Background/tsconfig.json", 12 | "./__mocks__/tsconfig.json" 13 | ], 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-console": "off", 18 | "no-extend-native": "off", 19 | "react/jsx-props-no-spreading": "off", 20 | "jsx-a11y/label-has-associated-control": "off", 21 | "class-methods-use-this": "off", 22 | "max-classes-per-file": "off", 23 | "node/no-missing-import": "off", 24 | "node/no-unpublished-import": "off", 25 | "node/no-unsupported-features/es-syntax": ["error", { 26 | "ignores": ["modules"] 27 | }], 28 | "@typescript-eslint/comma-dangle": "off", 29 | "@typescript-eslint/object-curly-spacing": "off", 30 | "@typescript-eslint/quotes": "off" 31 | }, 32 | "env": { 33 | "webextensions": true 34 | }, 35 | "settings": { 36 | "node": { 37 | "tryExtensions": [".tsx"] 38 | } 39 | }, 40 | "globals": { 41 | "NodeJS": true 42 | } 43 | } -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | groups: 8 | dependencies: 9 | applies-to: version-updates 10 | update-types: 11 | - "minor" 12 | - "patch" 13 | patterns: 14 | - "*" 15 | open-pull-requests-limit: 10 16 | - package-ecosystem: "github-actions" 17 | directory: "/" 18 | schedule: 19 | interval: "monthly" 20 | groups: 21 | gha: 22 | applies-to: version-updates 23 | patterns: 24 | - "*" 25 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Check CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | check: 10 | runs-on: ubuntu-22.04 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | 15 | - name: Set Node.js environment 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '20' 19 | 20 | - name: Install dependencies 21 | run: yarn install 22 | 23 | - name: Lint 24 | run: yarn lint 25 | 26 | - name: Build 27 | run: yarn run build 28 | 29 | - name: Install browsers for tests 30 | run: yarn playwright install --with-deps --no-shell 31 | 32 | - name: Test 33 | run: xvfb-run yarn test 34 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore haters 2 | haters/ 3 | 4 | ### WebStorm+all ### 5 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 6 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 7 | 8 | # User-specific stuff 9 | .idea/**/workspace.xml 10 | .idea/**/tasks.xml 11 | .idea/**/usage.statistics.xml 12 | .idea/**/dictionaries 13 | .idea/**/shelf 14 | 15 | # Generated files 16 | .idea/**/contentModel.xml 17 | 18 | # Sensitive or high-churn files 19 | .idea/**/dataSources/ 20 | .idea/**/dataSources.ids 21 | .idea/**/dataSources.local.xml 22 | .idea/**/sqlDataSources.xml 23 | .idea/**/dynamic.xml 24 | .idea/**/uiDesigner.xml 25 | .idea/**/dbnavigator.xml 26 | 27 | # Eclipse 28 | .metadata 29 | tmp/ 30 | *.tmp 31 | *.bak 32 | *.swp 33 | *~.nib 34 | local.properties 35 | .loadpath 36 | *.classpath 37 | *.project 38 | *.settings 39 | /bin 40 | RemoteSystemsTempFiles/ 41 | .externalToolBuilders/ 42 | *.launch 43 | .buildpath 44 | 45 | # Gradle 46 | .idea/**/gradle.xml 47 | .idea/**/libraries 48 | 49 | # Gradle and Maven with auto-import 50 | # When using Gradle or Maven with auto-import, you should exclude module files, 51 | # since they will be recreated, and may cause churn. Uncomment if using 52 | # auto-import. 53 | # .idea/artifacts 54 | # .idea/compiler.xml 55 | # .idea/jarRepositories.xml 56 | # .idea/modules.xml 57 | # .idea/*.iml 58 | # .idea/modules 59 | # *.iml 60 | # *.ipr 61 | 62 | # CMake 63 | cmake-build-*/ 64 | 65 | # Mongo Explorer plugin 66 | .idea/**/mongoSettings.xml 67 | 68 | # File-based project format 69 | *.iws 70 | 71 | # IntelliJ 72 | out/ 73 | 74 | # mpeltonen/sbt-idea plugin 75 | .idea_modules/ 76 | 77 | # JIRA plugin 78 | atlassian-ide-plugin.xml 79 | 80 | # Cursive Clojure plugin 81 | .idea/replstate.xml 82 | 83 | # Crashlytics plugin (for Android Studio and IntelliJ) 84 | com_crashlytics_export_strings.xml 85 | crashlytics.properties 86 | crashlytics-build.properties 87 | fabric.properties 88 | 89 | # Editor-based Rest Client 90 | .idea/httpRequests 91 | 92 | # Android studio 3.1+ serialized cache file 93 | .idea/caches/build_file_checksums.ser 94 | 95 | ### WebStorm+all Patch ### 96 | # Ignores the whole .idea folder and all .iml files 97 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 98 | 99 | .idea/ 100 | 101 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 102 | 103 | *.iml 104 | modules.xml 105 | .idea/misc.xml 106 | *.ipr 107 | 108 | # Sonarlint plugin 109 | .idea/sonarlint 110 | 111 | # End of https://www.toptal.com/developers/gitignore/api/webstorm+all 112 | 113 | ### Node ### 114 | # Logs 115 | logs 116 | *.log 117 | npm-debug.log* 118 | yarn-debug.log* 119 | yarn-error.log* 120 | lerna-debug.log* 121 | 122 | # Diagnostic reports (https://nodejs.org/api/report.html) 123 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 124 | 125 | # Runtime data 126 | pids 127 | *.pid 128 | *.seed 129 | *.pid.lock 130 | 131 | # Directory for instrumented libs generated by jscoverage/JSCover 132 | lib-cov 133 | 134 | # Coverage directory used by tools like istanbul 135 | coverage 136 | *.lcov 137 | 138 | # nyc test coverage 139 | .nyc_output 140 | 141 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 142 | .grunt 143 | 144 | # Bower dependency directory (https://bower.io/) 145 | bower_components 146 | 147 | # node-waf configuration 148 | .lock-wscript 149 | 150 | # Compiled binary addons (https://nodejs.org/api/addons.html) 151 | build/Release 152 | 153 | # Dependency directories 154 | node_modules/ 155 | jspm_packages/ 156 | 157 | # TypeScript v1 declaration files 158 | typings/ 159 | 160 | # TypeScript cache 161 | *.tsbuildinfo 162 | 163 | # Optional npm cache directory 164 | .npm 165 | 166 | # Optional eslint cache 167 | .eslintcache 168 | 169 | # Optional REPL history 170 | .node_repl_history 171 | 172 | # Output of 'npm pack' 173 | *.tgz 174 | 175 | # Yarn Integrity file 176 | .yarn-integrity 177 | 178 | # dotenv environment variables file 179 | .env 180 | .env.test 181 | 182 | # parcel-bundler cache (https://parceljs.org/) 183 | .cache 184 | 185 | # next.js build output 186 | .next 187 | 188 | # nuxt.js build output 189 | .nuxt 190 | 191 | # react / gatsby 192 | public/ 193 | 194 | # vuepress build output 195 | .vuepress/dist 196 | 197 | # Serverless directories 198 | .serverless/ 199 | 200 | # FuseBox cache 201 | .fusebox/ 202 | 203 | # DynamoDB Local files 204 | .dynamodb/ 205 | 206 | ### Sass ### 207 | .sass-cache/ 208 | *.css.map 209 | *.sass.map 210 | *.scss.map 211 | 212 | ## Build directory 213 | extension/ 214 | dist/ 215 | .awcache 216 | 217 | # yarn 2 218 | # https://github.com/yarnpkg/berry/issues/454#issuecomment-530312089 219 | .yarn/* 220 | !.yarn/releases 221 | !.yarn/plugins 222 | .pnp.* -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to the full browser extension will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## Unreleased 7 | 8 | ## 0.1.0 - 2025-06-06 9 | 10 | ### Added 11 | - Help screen 12 | 13 | ### Changed 14 | - Auto-download the script if stopped from the notification panel. 15 | 16 | ## 0.0.15 - 2025-05-28 17 | 18 | ### Added 19 | - Allow to provide the login URL through the recording panel. 20 | 21 | ## 0.0.14 - 2025-05-23 22 | 23 | ### Fixed 24 | - Initialize the Zest script also when injecting the content script to record the page accessed after starting the recording, as opposed to starting after accessing the page. 25 | 26 | ## 0.0.13 - 2025-05-09 27 | 28 | ### Added 29 | - Zest comment including the extension version when recording. 30 | - New recorder extension, which is a cut down version of the full one, without the capability to talk to ZAP. 31 | - Support for incognito mode. 32 | 33 | ## 0.0.12 - 2025-05-05 34 | 35 | ### Changed 36 | - When recording scroll to statements will be added prior to and associated with typing, clicks, and submissions. 37 | - Where practical statements are now recorded including a waitForMsec property value rounded up to the nearest 5sec. 38 | 39 | ### Removed 40 | - Clear statements before inputting text, they were not needed and could cause problems. 41 | 42 | ## 0.0.11 - 2025-01-17 43 | 44 | ### Added 45 | - Option to start recording when the browser is launched. 46 | 47 | ### Changed 48 | - The default to not send data to ZAP. 49 | 50 | ## 0.0.10 - 2024-12-20 51 | 52 | ### Changed 53 | - Report index of the forms. 54 | 55 | ## 0.0.9 - 2024-11-28 56 | 57 | ### Added 58 | - Support for Enter key in input fields. 59 | 60 | ### Changed 61 | - Branding to ZAP by Checkmarx. 62 | 63 | ## 0.0.8 - 2023-12-01 64 | 65 | ### Added 66 | - Input field type and form index. 67 | 68 | ### Changed 69 | - Poll for storage changes. 70 | 71 | ## 0.0.7 - 2023-10-23 72 | 73 | ### Changed 74 | - Init ZAP URL to http://zap/ instead of http://localhost:8080/ 75 | 76 | ### Fixed 77 | - Same links continually reported on domMutation events (Issue 81). 78 | 79 | ## 0.0.6 - 2023-09-19 80 | 81 | ### Fixed 82 | - Storage events not being reported. 83 | -------------------------------------------------------------------------------- /CHANGELOG.rec.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to the recorder browser extension will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 5 | 6 | ## Unreleased 7 | 8 | ## 0.1.0 - 2025-06-06 9 | 10 | ### Added 11 | - Help screen 12 | 13 | ### Changed 14 | - Auto-download the script if stopped from the notification panel. 15 | 16 | ## 0.0.3 - 2025-05-28 17 | 18 | ### Added 19 | - Allow to provide the login URL through the recording panel. 20 | 21 | ## 0.0.2 - 2025-05-23 22 | 23 | ### Fixed 24 | - Initialize the Zest script also when injecting the content script to record the page accessed after starting the recording, as opposed to starting after accessing the page. 25 | 26 | ## 0.0.1 - 2025-05-09 27 | 28 | ### Added 29 | - First version, recorder split from full extension. 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ZAP Browser Extensions 2 | 3 | This repo defines 2 related ZAP browser extensions. 4 | 5 | ## The 'Full' Extension 6 | 7 | A browser extension which allows [ZAP](https://www.zaproxy.org) to interact directly with the browser. 8 | It also allows you to record whatever you do in a browser as [Zest](https://github.com/zaproxy/zest) scripts. 9 | These can be used to handle complicated authentication flows or sequences of related actions. 10 | 11 | [![ZAP Chat: Modern Apps Part 1](https://img.youtube.com/vi/Rq_d7OLmMfw/0.jpg)](https://www.youtube.com/watch?v=Rq_d7OLmMfw) 12 | 13 | Works in both Firefox and Chrome. 14 | 15 | Initially generated from https://github.com/abhijithvijayan/web-extension-starter/tree/react-typescript 16 | 17 | Only Firefox and Chrome have been tested - Opera may or may not work :wink: 18 | 19 | This extension is bundled in the ZAP [Client Side Integration](https://www.zaproxy.org/docs/desktop/addons/client-side-integration/) 20 | add-on, so you typically do not need to install it manually. 21 | 22 | 23 | The latest published extensions are still available via the relevant stores: 24 | 25 | * Firefox [ZAP by Checkmarx Browser Extension](https://addons.mozilla.org/en-GB/firefox/addon/zap-browser-extension/) 26 | * Chrome [ZAP by Checkmarx Browser Extension](https://chromewebstore.google.com/detail/zap-by-checkmarx-browser/cgkggmillbmmpokepnicllalaohphffo) 27 | 28 | ## The Recorder Extension 29 | 30 | This extension only allows you to record [Zest](https://github.com/zaproxy/zest) scripts in the browser. 31 | It will not interact with ZAP, even if you have it running. 32 | 33 | You can use this extension to record Zest scripts on a system on which ZAP is not running. 34 | 35 | The latest published extensions will be available via the relevant stores: 36 | 37 | * Firefox - [ZAP by Checkmarx Recorder](https://addons.mozilla.org/en-GB/firefox/addon/zap-by-checkmarx-recorder/) 38 | * Chrome - [ZAP by Checkmarx Recorder](https://chromewebstore.google.com/detail/zap-by-checkmarx-recorder/belmenkmkfloppjbbgibipmgcmnkaiki) 39 | 40 | ## Quick Start 41 | 42 | Ensure you have 43 | 44 | - [Node.js](https://nodejs.org) 16 or later installed 45 | - [Yarn](https://yarnpkg.com) v1 or v2 installed 46 | 47 | Then run the following: 48 | 49 | - `yarn install` to install dependencies. 50 | - `yarn run dev:chrome` to start the development server for the full chrome extension 51 | - `yarn run dev:firefox` to start the development server for the full firefox addon 52 | - `yarn run dev:opera` to start the development server for the full opera extension 53 | - `yarn run build:ext:chrome` to build the full chrome extension 54 | - `yarn run build:ext:firefox` to build the full firefox addon 55 | - `yarn run build:ext:opera` to build the full opera extension 56 | - `yarn run build:ext` builds and packs the full extensions all at once to extension/ directory 57 | - `yarn run build:rec:chrome` to build the recorder chrome extension 58 | - `yarn run build:rec:firefox` to build the recorder firefox addon 59 | - `yarn run build:rec:opera` to build the recorder opera extension 60 | - `yarn run build:rec` builds and packs the recorder extensions all at once to extension/ directory 61 | - `yarn run build` builds and packs both the full and recorder extensions all at once to extension/ directory 62 | - `yarn run lint` to lint the code 63 | - `yarn run lint --fix` to fix any lint errors 64 | - `yarn playwright install` at least once before the tests 65 | - `yarn run test` to run the test suite (you should not have anything listening on port 8080) 66 | - Note that individual tests can be run like `yarn run test -t "Should report forms"` 67 | 68 | 69 | ### Development 70 | 71 | - `yarn install` to install dependencies. 72 | - To watch file changes in development 73 | 74 | - Chrome 75 | - `yarn run dev:chrome` 76 | - Firefox 77 | - `yarn run dev:firefox` 78 | - Opera 79 | - `yarn run dev:opera` 80 | 81 | - **Load extension in browser** 82 | 83 | - ### Chrome 84 | 85 | - Go to the browser address bar and type `chrome://extensions` 86 | - Check the `Developer Mode` button to enable it. 87 | - Click on the `Load Unpacked Extension…` button. 88 | - Select your browsers folder in `extension/`. 89 | 90 | - ### Firefox 91 | 92 | - Load the Add-on via `about:debugging` as temporary Add-on. 93 | - Choose the `manifest.json` file in the extracted directory 94 | 95 | - ### Opera 96 | 97 | - Load the extension via `opera:extensions` 98 | - Check the `Developer Mode` and load as unpacked from extension’s extracted directory. 99 | 100 | ### Production 101 | 102 | - `yarn run build` builds the extension for all the browsers to `extension/BROWSER` directory respectively. 103 | 104 | 105 | ### Linting & TypeScript Config 106 | 107 | - Shared Eslint & Prettier Configuration - [`@abhijithvijayan/eslint-config`](https://www.npmjs.com/package/@abhijithvijayan/eslint-config) 108 | - Shared TypeScript Configuration - [`@abhijithvijayan/tsconfig`](https://www.npmjs.com/package/@abhijithvijayan/tsconfig) 109 | 110 | ## Licenses 111 | 112 | ### ZAP Code 113 | 114 | All of the ZAP specific code is licensed under ApacheV2 © The ZAP Core Team 115 | 116 | ### Web Extension Starter 117 | 118 | The Web Extension Starter is licensed under MIT © [Abhijith Vijayan](https://abhijithvijayan.in) -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | # Releasing the Extensions 2 | 3 | 1. Update the versions in `source/manifest.json` and `source/manifest.rec.json` 4 | 1. Update `CHANGELOG.md` and `CHANGELOG.rec.md` with the new versions 5 | 1. Submit a PR with the above changes 6 | 1. Once the PR has been merged 7 | 1. Tag the full release e.g. `git tag -a v0.1.x -m "Release Full v0.1.x"` 8 | 1. Push the full tag e.g. `git push upstream v0.1.x` 9 | 1. Tag the recorder release e.g. `git tag -a rec-v0.1.x -m "Release Recorder v0.1.x"` 10 | 1. Push the recorder tag e.g. `git push upstream rec-v0.1.x` 11 | 1. Run `yarn run build` 12 | 1. Upload the extensions to Firefox Add-Ons and the Chrome Web Store 13 | 1. https://addons.mozilla.org/en-GB/developers/addon/zap-browser-extension/edit 14 | 1. https://addons.mozilla.org/en-GB/developers/addon/zap-by-checkmarx-recorder/edit 15 | 1. https://chrome.google.com/webstore/devconsole 16 | 1. Update `CHANGELOG.md` and `CHANGELOG.rec.md` with a new Unreleased section 17 | 1. Submit a PR with the above changes 18 | -------------------------------------------------------------------------------- /__mocks__/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "outDir": "../dist" 7 | }, 8 | "include": [ 9 | "./*.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /__mocks__/webextension-polyfill.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | const mockStorageSyncGet = jest.fn().mockImplementation(() => { 21 | return Promise.resolve({zapenable: false}); 22 | }); 23 | 24 | const Browser = { 25 | runtime: { 26 | sendMessage: jest.fn(), 27 | onMessage: { 28 | addListener: jest.fn(), 29 | }, 30 | onInstalled: { 31 | addListener: jest.fn(), 32 | }, 33 | }, 34 | storage: { 35 | sync: { 36 | get: mockStorageSyncGet, 37 | }, 38 | }, 39 | cookies: { 40 | onChanged: { 41 | addListener: jest.fn(), 42 | }, 43 | set: jest.fn().mockImplementation(() => 44 | Promise.resolve({ 45 | name: 'ZAP', 46 | value: 'Proxy', 47 | path: '/', 48 | domain: 'example.com', 49 | }) 50 | ), 51 | }, 52 | action: { 53 | onClicked: { 54 | addListener: jest.fn(), 55 | }, 56 | }, 57 | tabs: { 58 | query: jest.fn(), 59 | }, 60 | }; 61 | 62 | export default Browser; 63 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # ZAP Browser Extension Store Docs 2 | 3 | This directory contains the information and screenshots used for the browser stores: 4 | 5 | * [Firefox Add-Ons](https://addons.mozilla.org/en-GB/firefox/addon/zap-browser-extension/) 6 | * [Chrome Web Store](https://chrome.google.com/webstore/detail/zap-browser-extension/oeadiegekjdlhpooeidmimgnmbfllehp) 7 | 8 | ## Title 9 | 10 | ZAP by Checkmarx Browser Extension 11 | 12 | ## Summary 13 | 14 | Note that in the Chrome store the description in the manifest has a limit of 132 characters. 15 | 16 | ### Extension 17 | 18 | A browser extension which allows ZAP to access the client side of a web app. 19 | 20 | ### Recorder 21 | 22 | An extension for recording actions taken in the browser. It can be used to record things like auth scripts to be used in ZAP. 23 | 24 | ## Description 25 | 26 | ### Extension 27 | 28 | ZAP by Checkmarx is a free OSS security tool which can be used to test the security of your web apps. 29 | This browser extension will allow ZAP to gather more information about your app from the browser. 30 | Most users should not install this extension. 31 | It will be bundled in a ZAP add-on - this add-on will add the extension to Firefox/Chrome when it is launched from ZAP. 32 | 33 | ### Recorder 34 | 35 | ZAP by Checkmarx is a free OSS security tool which can be used to test the security of your web apps. 36 | This extension allows you to record all of the actions you take in the browser as a Zest script. 37 | It can be used to record things like authentication scripts or other complex interactions. 38 | Zest scripts can be replayed in ZAP, whether in the desktop or in automation. 39 | 40 | ## Category 41 | 42 | * Firefox: Privacy and Security 43 | * Chrome: Developer Tools 44 | 45 | ## Additional Fields 46 | 47 | Video: https://www.youtube.com/watch?v=Rq_d7OLmMfw 48 | 49 | Homepage: https://github.com/zaproxy/browser-extension 50 | 51 | Support URL: https://groups.google.com/group/zaproxy-users 52 | 53 | ## Chrome Privacy Practices 54 | 55 | ### Single Purpose 56 | 57 | This extension is designed for people testing the security of their own apps. We do not expect people to install it in their main Chrome profile, at least not at this stage. We will be bundling it with ZAP which will then add it to the Chrome instances that it launches - these will be in a temporary profile. 58 | 59 | ### Permission Justification 60 | 61 | #### Tabs Justification 62 | 63 | As mentioned above, this is a security extension which we do not expect people to install in their main Chrome profile. 64 | The extension streams a summary of the key events to ZAP - we do this by using event listeners and by polling, e.g. for storage changes. We need to know when the user leaves the current page in order to make a final check for things like local storage changes. 65 | 66 | #### Cookies Justification 67 | 68 | As a security tool it is essential to see and report what cookies are being set. This is a key feature of this extension. 69 | 70 | #### Storage Justification 71 | 72 | As a security tool it is essential to see and report what is being put in local storage. This is a key feature of this extension. 73 | 74 | #### Host Permission Justification 75 | 76 | It's a security tool :) We have no idea which sites a security person will want to test. 77 | 78 | #### Privacy Policy URL 79 | 80 | https://www.zaproxy.org/faq/what-data-does-zap-collect/ 81 | -------------------------------------------------------------------------------- /docs/screenshots/zap-chrome-options-smaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-chrome-options-smaller.png -------------------------------------------------------------------------------- /docs/screenshots/zap-chrome-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-chrome-options.png -------------------------------------------------------------------------------- /docs/screenshots/zap-client-windows-smaller.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-client-windows-smaller.png -------------------------------------------------------------------------------- /docs/screenshots/zap-client-windows.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-client-windows.png -------------------------------------------------------------------------------- /docs/screenshots/zap-firefox-options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-firefox-options.png -------------------------------------------------------------------------------- /docs/screenshots/zap-record.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/screenshots/zap-record.png -------------------------------------------------------------------------------- /docs/zap128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/docs/zap128x128.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zap-extension", 3 | "version": "0.0.12", 4 | "description": "ZAP by Checkmarx Browser Extension - allows ZAP to interact directly with the browser.", 5 | "private": true, 6 | "repository": "https://github.com/zaproxy/browser-extension/", 7 | "author": { 8 | "name": "ZAP Core Team", 9 | "email": "zaproxy-admin@googlegroups.com", 10 | "url": "https://zaproxy.org" 11 | }, 12 | "license": "MIT", 13 | "engines": { 14 | "node": ">=16.0.0", 15 | "yarn": ">= 1.0.0" 16 | }, 17 | "jest": { 18 | "preset": "ts-jest", 19 | "testEnvironment": "jest-environment-node" 20 | }, 21 | "scripts": { 22 | "dev:chrome": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=chrome webpack --watch", 23 | "dev:firefox": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=firefox webpack --watch", 24 | "dev:opera": "cross-env NODE_ENV=development cross-env TARGET_BROWSER=opera webpack --watch", 25 | "build:ext:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack", 26 | "build:ext:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack", 27 | "build:ext:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack", 28 | "build:ext": "yarn run build:ext:chrome && yarn run build:ext:firefox && yarn run build:ext:opera", 29 | 30 | "build:rec:chrome": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=chrome webpack --config webpack.config.rec.js", 31 | "build:rec:firefox": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=firefox webpack --config webpack.config.rec.js", 32 | "build:rec:opera": "cross-env NODE_ENV=production cross-env TARGET_BROWSER=opera webpack --config webpack.config.rec.js", 33 | "build:rec": "yarn run build:rec:chrome && yarn run build:rec:firefox && yarn run build:rec:opera", 34 | "build": "yarn run build:ext && yarn run build:rec", 35 | 36 | "lint": "eslint . --ext .ts,.tsx", 37 | "lint:fix": "eslint . --ext .ts,.tsx --fix", 38 | "test": "jest --coverage --testTimeout=10000", 39 | "buildAndTest": "yarn run build && yarn test" 40 | }, 41 | "dependencies": { 42 | "@babel/runtime": "^7.27.4", 43 | "advanced-css-reset": "^2.1.3", 44 | "emoji-log": "^1.0.2", 45 | "i18next": "^25.2.1", 46 | "i18next-browser-languagedetector": "^8.1.0", 47 | "lodash": "^4.17.21", 48 | "webext-base-css": "^2.1.0", 49 | "webextension-polyfill": "0.12.0" 50 | }, 51 | "devDependencies": { 52 | "4.5": "^0.5.0", 53 | "@abhijithvijayan/eslint-config": "2.8.1", 54 | "@abhijithvijayan/eslint-config-airbnb": "^1.0.2", 55 | "@abhijithvijayan/tsconfig": "^1.3.0", 56 | "@babel/core": "^7.27.4", 57 | "@babel/eslint-parser": "^7.27.1", 58 | "@babel/plugin-proposal-class-properties": "^7.13.0", 59 | "@babel/plugin-proposal-object-rest-spread": "^7.14.2", 60 | "@babel/plugin-transform-destructuring": "^7.27.3", 61 | "@babel/plugin-transform-runtime": "^7.27.4", 62 | "@babel/preset-env": "^7.27.2", 63 | "@babel/preset-react": "^7.27.1", 64 | "@babel/preset-typescript": "^7.27.1", 65 | "@jest/globals": "^29.4.3", 66 | "@playwright/test": "^1.52.0", 67 | "@types/jest": "^29.4.0", 68 | "@types/json-server": "^0.14.8", 69 | "@types/lodash": "^4.17.17", 70 | "@types/node": "^22.15.29", 71 | "@types/text-encoding": "^0.0.40", 72 | "@types/webextension-polyfill": "^0.12.3", 73 | "@types/webpack": "^5.28.5", 74 | "@typescript-eslint/eslint-plugin": "^5.59.11", 75 | "@typescript-eslint/parser": "^5.62.0", 76 | "autoprefixer": "^10.4.21", 77 | "babel-loader": "^10.0.0", 78 | "clean-webpack-plugin": "^4.0.0", 79 | "copy-webpack-plugin": "^13.0.0", 80 | "cross-env": "^7.0.3", 81 | "css-loader": "^7.1.2", 82 | "eslint": "^7.27.0", 83 | "eslint-config-prettier": "^10.1.5", 84 | "eslint-plugin-import": "^2.23.3", 85 | "eslint-plugin-jsx-a11y": "^6.4.1", 86 | "eslint-plugin-node": "^11.1.0", 87 | "eslint-plugin-prettier": "^4.2.1", 88 | "eslint-plugin-react": "^7.37.5", 89 | "eslint-plugin-react-hooks": "^4.6.2", 90 | "express": "^5.1.0", 91 | "filemanager-webpack-plugin": "^8.0.0", 92 | "fork-ts-checker-webpack-plugin": "^9.1.0", 93 | "html-webpack-plugin": "^5.5.0", 94 | "http-server": "^14.1.1", 95 | "jest": "^29.4.3", 96 | "jest-environment-jsdom": "^29.5.0", 97 | "jsdom": "^26.1.0", 98 | "json-server": "^0.17.3", 99 | "mini-css-extract-plugin": "^2.9.2", 100 | "optimize-css-assets-webpack-plugin": "^6.0.1", 101 | "playwright": "1.52.0", 102 | "playwright-webextext": "^0.0.4", 103 | "postcss": "^8.5.4", 104 | "postcss-loader": "^8.1.1", 105 | "prettier": "^2.3.0", 106 | "resolve-url-loader": "^5.0.0", 107 | "sass": "^1.89.1", 108 | "sass-loader": "^16.0.5", 109 | "terser-webpack-plugin": "^5.3.14", 110 | "ts-jest": "^29.3.4", 111 | "typescript": "^5.8.3", 112 | "webpack": "^5.99.9", 113 | "webpack-cli": "^6.0.1", 114 | "webpack-ext-reloader": "^1.1.9", 115 | "wext-manifest-loader": "^2.3.0", 116 | "wext-manifest-webpack-plugin": "^1.2.1" 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /source/Background/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import 'emoji-log'; 21 | import Browser, {Cookies, Runtime} from 'webextension-polyfill'; 22 | import {ReportedStorage} from '../types/ReportedModel'; 23 | import {ZestScript, ZestScriptMessage} from '../types/zestScript/ZestScript'; 24 | import {ZestStatementWindowClose} from '../types/zestScript/ZestStatement'; 25 | import { 26 | DOWNLOAD_RECORDING, 27 | GET_ZEST_SCRIPT, 28 | IS_FULL_EXTENSION, 29 | LOCAL_STORAGE, 30 | REPORT_EVENT, 31 | REPORT_OBJECT, 32 | RESET_ZEST_SCRIPT, 33 | SESSION_STORAGE, 34 | SET_SAVE_SCRIPT_ENABLE, 35 | STOP_RECORDING, 36 | ZEST_SCRIPT, 37 | } from '../utils/constants'; 38 | 39 | console.log('ZAP Service Worker 👋'); 40 | 41 | /* 42 | 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. 43 | */ 44 | const reportedStorage = new Set(); 45 | const zestScript = new ZestScript(); 46 | /* 47 | A callback URL will only be available if the browser has been launched from ZAP, otherwise call the individual endpoints 48 | */ 49 | 50 | function zapApiUrl(zapurl: string, action: string): string { 51 | if (zapurl.indexOf('/zapCallBackUrl/') > 0) { 52 | return zapurl; 53 | } 54 | return `${zapurl}JSON/client/action/${action}/`; 55 | } 56 | 57 | function getUrlFromCookieDomain(domain: string): string { 58 | return domain.startsWith('.') 59 | ? `http://${domain.substring(1)}` 60 | : `http://${domain}`; 61 | } 62 | 63 | function getCookieTabUrl(cookie: Cookies.Cookie): Promise { 64 | const getAllTabs = Browser.tabs.query({ 65 | currentWindow: true, 66 | }); 67 | return new Promise((resolve, reject) => { 68 | getAllTabs 69 | .then((allTabs) => { 70 | for (const tab of allTabs) { 71 | if (tab.url) { 72 | const getAllCookiesForTab = Browser.cookies.getAll({url: tab.url}); 73 | getAllCookiesForTab.then((cookies) => { 74 | for (const c of cookies) { 75 | if ( 76 | c.name === cookie.name && 77 | c.value === cookie.value && 78 | c.domain === cookie.domain && 79 | c.storeId === cookie.storeId 80 | ) { 81 | resolve( 82 | tab.url ? tab.url : getUrlFromCookieDomain(cookie.domain) 83 | ); 84 | } 85 | } 86 | }); 87 | } 88 | } 89 | }) 90 | .catch((error) => { 91 | console.error(`Could not fetch tabs: ${error.message}`); 92 | reject(getUrlFromCookieDomain(cookie.domain)); 93 | }); 94 | }); 95 | } 96 | 97 | function reportCookies( 98 | cookie: Cookies.Cookie, 99 | zapurl: string, 100 | zapkey: string 101 | ): boolean { 102 | let cookieString = `${cookie.name}=${cookie.value}; path=${cookie.path}; domain=${cookie.domain}`; 103 | if (cookie.expirationDate) { 104 | cookieString = cookieString.concat( 105 | `; expires=${new Date(cookie.expirationDate * 1000).toUTCString()}` 106 | ); 107 | } 108 | if (cookie.secure) { 109 | cookieString = cookieString.concat(`; secure`); 110 | } 111 | if (cookie.sameSite === 'lax' || cookie.sameSite === 'strict') { 112 | cookieString = cookieString.concat(`; SameSite=${cookie.sameSite}`); 113 | } 114 | if (cookie.httpOnly) { 115 | cookieString = cookieString.concat(`; HttpOnly`); 116 | } 117 | 118 | getCookieTabUrl(cookie) 119 | .then((cookieUrl) => { 120 | const repStorage = new ReportedStorage( 121 | 'Cookies', 122 | '', 123 | cookie.name, 124 | '', 125 | cookieString, 126 | cookieUrl 127 | ); 128 | const repStorStr: string = repStorage.toShortString(); 129 | if ( 130 | !reportedStorage.has(repStorStr) && 131 | repStorage.url.startsWith('http') 132 | ) { 133 | const body = `objectJson=${encodeURIComponent( 134 | repStorage.toString() 135 | )}&apikey=${encodeURIComponent(zapkey)}`; 136 | 137 | fetch(zapApiUrl(zapurl, REPORT_OBJECT), { 138 | method: 'POST', 139 | body, 140 | headers: { 141 | 'Content-Type': 'application/x-www-form-urlencoded', 142 | }, 143 | }); 144 | 145 | reportedStorage.add(repStorStr); 146 | } 147 | }) 148 | .catch((error) => { 149 | console.log(error); 150 | return false; 151 | }); 152 | 153 | return true; 154 | } 155 | 156 | function sendZestScriptToZAP( 157 | data: string, 158 | zapkey: string, 159 | zapurl: string 160 | ): void { 161 | if (IS_FULL_EXTENSION) { 162 | const body = `statementJson=${encodeURIComponent( 163 | data 164 | )}&apikey=${encodeURIComponent(zapkey)}`; 165 | console.log(`body = ${body}`); 166 | fetch(zapApiUrl(zapurl, 'reportZestStatement'), { 167 | method: 'POST', 168 | body, 169 | headers: { 170 | 'Content-Type': 'application/x-www-form-urlencoded', 171 | }, 172 | }); 173 | } 174 | } 175 | 176 | function downloadZestScript(zestScriptJSON: string, title: string): void { 177 | const blob = new Blob([zestScriptJSON], {type: 'application/json'}); 178 | const url = URL.createObjectURL(blob); 179 | 180 | const link = document.createElement('a'); 181 | link.href = url; 182 | link.download = title + (title.slice(-4) === '.zst' ? '' : '.zst'); 183 | link.style.display = 'none'; 184 | 185 | document.body.appendChild(link); 186 | link.click(); 187 | document.body.removeChild(link); 188 | 189 | URL.revokeObjectURL(url); 190 | } 191 | 192 | function pad(i: number): string { 193 | return `${i}`.padStart(2, `0`); 194 | } 195 | 196 | function getDateString(): string { 197 | const now = new Date(); 198 | return `${now.getFullYear()}-${pad(now.getMonth())}-${pad( 199 | now.getDay() 200 | )}-${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`; 201 | } 202 | 203 | async function handleMessage( 204 | request: MessageEvent, 205 | zapurl: string, 206 | zapkey: string 207 | ): Promise { 208 | console.log(`ZAP Service worker calling ZAP on ${zapurl}`); 209 | console.log(zapApiUrl(zapurl, REPORT_OBJECT)); 210 | console.log(encodeURIComponent(zapkey)); 211 | console.log(`Type: ${request.type}`); 212 | console.log(`Data: ${request.data}`); 213 | switch (request.type) { 214 | case REPORT_OBJECT: { 215 | const repObj = JSON.parse(request.data); 216 | if (repObj.type === LOCAL_STORAGE || repObj.type === SESSION_STORAGE) { 217 | const repStorage = new ReportedStorage('', '', '', '', '', ''); 218 | Object.assign(repStorage, repObj); 219 | const repStorStr = repStorage.toShortString(); 220 | if (reportedStorage.has(repStorStr)) { 221 | return true; 222 | } 223 | reportedStorage.add(repStorStr); 224 | } 225 | const repObjBody = `objectJson=${encodeURIComponent( 226 | request.data 227 | )}&apikey=${encodeURIComponent(zapkey)}`; 228 | console.log(`body = ${repObjBody}`); 229 | fetch(zapApiUrl(zapurl, REPORT_OBJECT), { 230 | method: 'POST', 231 | body: repObjBody, 232 | headers: { 233 | 'Content-Type': 'application/x-www-form-urlencoded', 234 | }, 235 | }); 236 | break; 237 | } 238 | 239 | case REPORT_EVENT: { 240 | const eventBody = `eventJson=${encodeURIComponent( 241 | request.data 242 | )}&apikey=${encodeURIComponent(zapkey)}`; 243 | console.log(`body = ${eventBody}`); 244 | fetch(zapApiUrl(zapurl, REPORT_EVENT), { 245 | method: 'POST', 246 | body: eventBody, 247 | headers: { 248 | 'Content-Type': 'application/x-www-form-urlencoded', 249 | }, 250 | }); 251 | break; 252 | } 253 | 254 | case ZEST_SCRIPT: { 255 | const data = zestScript.addStatement(request.data); 256 | sendZestScriptToZAP(data, zapkey, zapurl); 257 | break; 258 | } 259 | 260 | case GET_ZEST_SCRIPT: 261 | return zestScript.getZestScript(); 262 | 263 | case RESET_ZEST_SCRIPT: 264 | zestScript.reset(); 265 | break; 266 | 267 | case STOP_RECORDING: { 268 | if (zestScript.getZestStatementCount() > 0) { 269 | const {zapclosewindowhandle} = await Browser.storage.sync.get({ 270 | zapclosewindowhandle: false, 271 | }); 272 | if (zapclosewindowhandle) { 273 | const stmt = new ZestStatementWindowClose(0); 274 | const data = zestScript.addStatement(stmt.toJSON()); 275 | sendZestScriptToZAP(data, zapkey, zapurl); 276 | } 277 | } 278 | break; 279 | } 280 | case DOWNLOAD_RECORDING: { 281 | zestScript.getZestScript().then((items) => { 282 | const msg = items as ZestScriptMessage; 283 | let site = ''; 284 | if (request.data) { 285 | site = `${request.data}-`; 286 | } 287 | downloadZestScript(msg.script, `zap-rec-${site}${getDateString()}.zst`); 288 | }); 289 | break; 290 | } 291 | case SET_SAVE_SCRIPT_ENABLE: 292 | Browser.storage.sync.set({ 293 | zapenablesavescript: zestScript.getZestStatementCount() > 0, 294 | }); 295 | break; 296 | 297 | default: 298 | // Handle unknown request type 299 | break; 300 | } 301 | 302 | return true; 303 | } 304 | 305 | async function onMessageHandler( 306 | message: unknown, 307 | _sender: Runtime.MessageSender 308 | ): Promise { 309 | let val: number | ZestScriptMessage = 2; 310 | const items = await Browser.storage.sync.get({ 311 | zapurl: 'http://zap/', 312 | zapkey: 'not set', 313 | }); 314 | const msg = await handleMessage( 315 | message as MessageEvent, 316 | items.zapurl as string, 317 | items.zapkey as string 318 | ); 319 | if (!(typeof msg === 'boolean')) { 320 | val = msg; 321 | } 322 | return Promise.resolve(val); 323 | } 324 | 325 | function cookieChangeHandler( 326 | changeInfo: Cookies.OnChangedChangeInfoType 327 | ): void { 328 | Browser.storage.sync 329 | .get({ 330 | zapurl: 'http://zap/', 331 | zapkey: 'not set', 332 | }) 333 | .then((items) => { 334 | reportCookies( 335 | changeInfo.cookie, 336 | items.zapurl as string, 337 | items.zapkey as string 338 | ); 339 | }); 340 | } 341 | 342 | Browser.runtime.onMessage.addListener(onMessageHandler); 343 | 344 | if (IS_FULL_EXTENSION) { 345 | Browser.action.onClicked.addListener((_tab: Browser.Tabs.Tab) => { 346 | Browser.runtime.openOptionsPage(); 347 | }); 348 | 349 | Browser.cookies.onChanged.addListener(cookieChangeHandler); 350 | 351 | Browser.runtime.onInstalled.addListener((): void => { 352 | console.emoji('🦄', 'extension installed'); 353 | Browser.storage.sync.set({ 354 | zapurl: 'http://zap/', 355 | zapkey: 'not set', 356 | }); 357 | }); 358 | } 359 | 360 | export {reportCookies}; 361 | -------------------------------------------------------------------------------- /source/ContentScript/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import Browser, {Runtime} from 'webextension-polyfill'; 21 | import { 22 | ReportedElement, 23 | ReportedObject, 24 | ReportedStorage, 25 | ReportedEvent, 26 | } from '../types/ReportedModel'; 27 | import Recorder from './recorder'; 28 | import { 29 | IS_FULL_EXTENSION, 30 | LOCAL_STORAGE, 31 | LOCAL_ZAP_ENABLE, 32 | LOCAL_ZAP_RECORD, 33 | REPORT_EVENT, 34 | REPORT_OBJECT, 35 | SESSION_STORAGE, 36 | URL_ZAP_ENABLE, 37 | URL_ZAP_RECORD, 38 | ZAP_START_RECORDING, 39 | ZAP_STOP_RECORDING, 40 | } from '../utils/constants'; 41 | 42 | const reportedObjects = new Set(); 43 | 44 | const reportedEvents: {[key: string]: ReportedEvent} = {}; 45 | 46 | const recorder = new Recorder(); 47 | 48 | function reportStorage( 49 | name: string, 50 | storage: Storage, 51 | fn: (re: ReportedStorage) => void 52 | ): void { 53 | if (IS_FULL_EXTENSION) { 54 | const url = window.location.href; 55 | for (const key of Object.keys(storage)) { 56 | fn(new ReportedStorage(name, '', key, '', storage.getItem(key), url)); 57 | } 58 | } 59 | } 60 | 61 | async function sendEventToZAP(obj: ReportedEvent): Promise { 62 | if (IS_FULL_EXTENSION) { 63 | return Browser.runtime.sendMessage({ 64 | type: REPORT_EVENT, 65 | data: obj.toString(), 66 | }); 67 | } 68 | return -1; 69 | } 70 | 71 | async function sendObjectToZAP(obj: ReportedObject): Promise { 72 | if (IS_FULL_EXTENSION) { 73 | return Browser.runtime.sendMessage({ 74 | type: REPORT_OBJECT, 75 | data: obj.toString(), 76 | }); 77 | } 78 | return -1; 79 | } 80 | 81 | function reportObject(repObj: ReportedObject): void { 82 | const repObjStr = repObj.toShortString(); 83 | if (!reportedObjects.has(repObjStr)) { 84 | sendObjectToZAP(repObj); 85 | reportedObjects.add(repObjStr); 86 | } 87 | } 88 | 89 | function reportAllStorage(): void { 90 | reportStorage(LOCAL_STORAGE, localStorage, reportObject); 91 | reportStorage(SESSION_STORAGE, sessionStorage, reportObject); 92 | } 93 | 94 | function withZapEnableSetting(fn: () => void): void { 95 | Browser.storage.sync.get({zapenable: false}).then((items) => { 96 | if (items.zapenable) { 97 | fn(); 98 | } 99 | }); 100 | } 101 | 102 | function withZapRecordingActive(fn: () => void): void { 103 | Browser.storage.sync.get({zaprecordingactive: false}).then((items) => { 104 | if (items.zaprecordingactive) { 105 | fn(); 106 | } 107 | }); 108 | } 109 | 110 | function reportPageUnloaded(): void { 111 | withZapEnableSetting(() => { 112 | Browser.runtime.sendMessage({ 113 | type: REPORT_EVENT, 114 | data: new ReportedEvent('pageUnload').toString(), 115 | }); 116 | for (const value of Object.values(reportedEvents)) { 117 | sendEventToZAP(value); 118 | } 119 | reportAllStorage(); 120 | }); 121 | } 122 | 123 | function reportEvent(event: ReportedEvent): void { 124 | let existingEvent: ReportedEvent; 125 | existingEvent = reportedEvents[event.eventName]; 126 | if (!existingEvent) { 127 | existingEvent = new ReportedEvent(event.eventName); 128 | reportedEvents[event.eventName] = event; 129 | sendEventToZAP(existingEvent); 130 | } else if (existingEvent.url !== window.location.href) { 131 | // The fragment has changed - report the old one and start a new count 132 | sendEventToZAP(existingEvent); 133 | existingEvent = new ReportedEvent(event.eventName); 134 | reportedEvents[event.eventName] = event; 135 | sendEventToZAP(existingEvent); 136 | } else { 137 | // eslint-disable-next-line no-plusplus 138 | existingEvent.count++; 139 | } 140 | } 141 | 142 | function reportPageForms( 143 | doc: Document, 144 | fn: (re: ReportedObject) => void 145 | ): void { 146 | const url = window.location.href; 147 | Array.prototype.forEach.call(doc.forms, (form: HTMLFormElement) => { 148 | fn(new ReportedElement(form, url)); 149 | }); 150 | } 151 | 152 | function reportPageLinks( 153 | doc: Document, 154 | fn: (re: ReportedObject) => void 155 | ): void { 156 | const url = window.location.href; 157 | Array.prototype.forEach.call( 158 | doc.links, 159 | (link: HTMLAnchorElement | HTMLAreaElement) => { 160 | fn(new ReportedElement(link, url)); 161 | } 162 | ); 163 | } 164 | 165 | function reportElements( 166 | collection: HTMLCollection, 167 | fn: (re: ReportedObject) => void 168 | ): void { 169 | const url = window.location.href; 170 | Array.prototype.forEach.call(collection, (element: Element) => { 171 | fn(new ReportedElement(element, url)); 172 | }); 173 | } 174 | 175 | function reportNodeElements( 176 | node: Node, 177 | tagName: string, 178 | fn: (re: ReportedObject) => void 179 | ): void { 180 | if (node.nodeType === Node.ELEMENT_NODE) { 181 | reportElements((node as Element).getElementsByTagName(tagName), fn); 182 | } 183 | } 184 | 185 | function reportPageLoaded( 186 | doc: Document, 187 | fn: (re: ReportedObject) => void 188 | ): void { 189 | Browser.runtime.sendMessage({ 190 | type: REPORT_EVENT, 191 | data: new ReportedEvent('pageLoad').toString(), 192 | }); 193 | 194 | reportPageLinks(doc, fn); 195 | reportPageForms(doc, fn); 196 | reportElements(doc.getElementsByTagName('input'), fn); 197 | reportElements(doc.getElementsByTagName('button'), fn); 198 | reportStorage(LOCAL_STORAGE, localStorage, fn); 199 | reportStorage(SESSION_STORAGE, sessionStorage, fn); 200 | } 201 | 202 | const domMutated = function domMutation( 203 | mutationList: MutationRecord[], 204 | _obs: MutationObserver 205 | ): void { 206 | withZapEnableSetting(() => { 207 | reportEvent(new ReportedEvent('domMutation')); 208 | reportPageLinks(document, reportObject); 209 | reportPageForms(document, reportObject); 210 | for (const mutation of mutationList) { 211 | if (mutation.type === 'childList') { 212 | reportNodeElements(mutation.target, 'input', reportObject); 213 | reportNodeElements(mutation.target, 'button', reportObject); 214 | } 215 | } 216 | }); 217 | }; 218 | 219 | function isConfigurationRequest(): boolean { 220 | return window.location.href.startsWith('https://zap/zapCallBackUrl/'); 221 | } 222 | 223 | function onLoadEventListener(): void { 224 | Browser.storage.sync.set({zaprecordingactive: false}); 225 | if (isConfigurationRequest()) { 226 | return; 227 | } 228 | 229 | withZapEnableSetting(() => { 230 | reportPageLoaded(document, reportObject); 231 | }); 232 | } 233 | 234 | function enableExtension(): void { 235 | window.addEventListener('load', onLoadEventListener, false); 236 | window.onbeforeunload = reportPageUnloaded; 237 | 238 | const observer = new MutationObserver(domMutated); 239 | observer.observe(document, { 240 | attributes: false, 241 | childList: true, 242 | subtree: true, 243 | }); 244 | 245 | setInterval(() => { 246 | // Have to poll to pickup storage changes in a timely fashion 247 | reportAllStorage(); 248 | }, 500); 249 | 250 | // This is needed for more traditional apps 251 | reportPageLoaded(document, reportObject); 252 | } 253 | 254 | function configureExtension(): void { 255 | if (isConfigurationRequest()) { 256 | // The Browser has been launched from ZAP - use this URL for configuration 257 | const params = new URLSearchParams(window.location.search); 258 | const enable = 259 | localStorage.getItem(LOCAL_ZAP_ENABLE) === 'true' || 260 | params.has(URL_ZAP_ENABLE); 261 | const record = 262 | localStorage.getItem(LOCAL_ZAP_RECORD) === 'true' || 263 | params.has(URL_ZAP_RECORD); 264 | 265 | console.log('ZAP Configure', enable, record); 266 | Browser.storage.sync.set({ 267 | zapurl: window.location.href.split('?')[0], 268 | zapenable: enable, 269 | zaprecordingactive: record, 270 | }); 271 | } 272 | } 273 | 274 | function injectScript(): Promise { 275 | return new Promise((resolve) => { 276 | configureExtension(); 277 | withZapRecordingActive(() => { 278 | Browser.storage.sync 279 | .get({initScript: false, loginUrl: ''}) 280 | .then((items) => { 281 | recorder.recordUserInteractions( 282 | items.initScript === true, 283 | items.loginUrl as string 284 | ); 285 | }); 286 | }); 287 | withZapEnableSetting(() => { 288 | enableExtension(); 289 | resolve(true); 290 | }); 291 | resolve(false); 292 | }); 293 | } 294 | 295 | injectScript(); 296 | 297 | /* eslint-disable @typescript-eslint/no-explicit-any */ 298 | Browser.runtime.onMessage.addListener( 299 | ( 300 | message: any, 301 | _sender: Runtime.MessageSender, 302 | _sendResponse: (response?: any) => void 303 | ) => { 304 | if (message.type === ZAP_START_RECORDING) { 305 | configureExtension(); 306 | recorder.recordUserInteractions(); 307 | } else if (message.type === ZAP_STOP_RECORDING) { 308 | recorder.stopRecordingUserInteractions(); 309 | } 310 | 311 | // Returning `true` keeps the message channel open for async responses 312 | return true; 313 | } 314 | ); 315 | 316 | export { 317 | reportPageLinks, 318 | reportPageLoaded, 319 | reportPageForms, 320 | reportNodeElements, 321 | reportStorage, 322 | ReportedElement, 323 | ReportedObject, 324 | ReportedStorage, 325 | ReportedEvent, 326 | injectScript, 327 | enableExtension, 328 | }; 329 | -------------------------------------------------------------------------------- /source/ContentScript/recorder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import debounce from 'lodash/debounce'; 21 | import Browser from 'webextension-polyfill'; 22 | import { 23 | ZestStatement, 24 | ZestStatementComment, 25 | ZestStatementElementClick, 26 | ZestStatementElementScrollTo, 27 | ZestStatementElementSendKeys, 28 | ZestStatementElementSubmit, 29 | ZestStatementLaunchBrowser, 30 | ZestStatementSwitchToFrame, 31 | } from '../types/zestScript/ZestStatement'; 32 | import {getPath} from './util'; 33 | import { 34 | DOWNLOAD_RECORDING, 35 | STOP_RECORDING, 36 | ZEST_SCRIPT, 37 | } from '../utils/constants'; 38 | 39 | const STOP_RECORDING_ID = 'ZAP-stop-recording-button'; 40 | const STOP_RECORDING_TEXT = 'Stop and Download Recording'; 41 | 42 | class Recorder { 43 | readonly timeAdjustmentMillis: number = 3000; 44 | 45 | readonly minimumWaitTimeInMillis: number = 5000; 46 | 47 | previousDOMState: string; 48 | 49 | curLevel = -1; 50 | 51 | curFrame = 0; 52 | 53 | active = false; 54 | 55 | haveListenersBeenAdded = false; 56 | 57 | floatingWindowInserted = false; 58 | 59 | isNotificationRaised = false; 60 | 61 | // Enter keydown events often occur before the input change events, so we reorder them if needed 62 | cachedSubmit?: ZestStatementElementSubmit; 63 | 64 | // We can get duplicate events for the enter key, this allows us to dedup them 65 | cachedTimeStamp = -1; 66 | 67 | lastStatementTime: number; 68 | 69 | async sendZestScriptToZAP( 70 | zestStatement: ZestStatement, 71 | sendCache = true 72 | ): Promise { 73 | if (sendCache) { 74 | this.handleCachedSubmit(); 75 | } 76 | // console.log('Sending statement', zestStatement); 77 | this.notify(zestStatement); 78 | return Browser.runtime.sendMessage({ 79 | type: ZEST_SCRIPT, 80 | data: zestStatement.toJSON(), 81 | }); 82 | } 83 | 84 | getWaited(): number { 85 | if (this.lastStatementTime === undefined || this.lastStatementTime === 0) { 86 | this.lastStatementTime = Date.now(); 87 | } 88 | // Adjust start time 89 | const lastStmtTime = this.lastStatementTime - this.timeAdjustmentMillis; 90 | // Round to nearest minimum (in millis) 91 | const waited = 92 | Math.ceil((Date.now() - lastStmtTime) / this.minimumWaitTimeInMillis) * 93 | this.minimumWaitTimeInMillis; 94 | this.lastStatementTime = Date.now(); 95 | return waited; 96 | } 97 | 98 | handleCachedSubmit(): void { 99 | if (this.cachedSubmit) { 100 | this.sendZestScriptToZAP( 101 | new ZestStatementElementScrollTo( 102 | this.cachedSubmit.elementLocator, 103 | this.getWaited() 104 | ), 105 | false 106 | ); 107 | // console.log('Sending cached submit', this.cachedSubmit); 108 | this.sendZestScriptToZAP(this.cachedSubmit, false); 109 | delete this.cachedSubmit; 110 | this.cachedTimeStamp = -1; 111 | } 112 | } 113 | 114 | handleFrameSwitches(level: number, frameIndex: number): void { 115 | if (this.curLevel === level && this.curFrame === frameIndex) { 116 | return; 117 | } 118 | if (this.curLevel > level) { 119 | while (this.curLevel > level) { 120 | this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(-1)); 121 | this.curLevel -= 1; 122 | } 123 | this.curFrame = frameIndex; 124 | } else { 125 | this.curLevel += 1; 126 | this.curFrame = frameIndex; 127 | this.sendZestScriptToZAP(new ZestStatementSwitchToFrame(frameIndex)); 128 | } 129 | if (this.curLevel !== level) { 130 | console.log('Error in switching frames'); 131 | } 132 | } 133 | 134 | handleClick( 135 | params: {level: number; frame: number; element: Document}, 136 | event: Event 137 | ): void { 138 | if (!this.shouldRecord(event.target as HTMLElement)) return; 139 | const waited: number = this.getWaited(); 140 | const {level, frame, element} = params; 141 | this.handleFrameSwitches(level, frame); 142 | console.log(event, 'clicked'); 143 | const elementLocator = getPath(event.target as HTMLElement, element); 144 | this.sendZestScriptToZAP( 145 | new ZestStatementElementScrollTo(elementLocator, this.getWaited()), 146 | false 147 | ); 148 | this.sendZestScriptToZAP( 149 | new ZestStatementElementClick(elementLocator, waited) 150 | ); 151 | // click on target element 152 | } 153 | 154 | handleScroll(params: {level: number; frame: number}, event: Event): void { 155 | if (!this.shouldRecord(event.target as HTMLElement)) return; 156 | const {level, frame} = params; 157 | this.handleFrameSwitches(level, frame); 158 | console.log(event, 'scrolling.. '); 159 | // scroll the nearest ancestor with scrolling ability 160 | } 161 | 162 | handleMouseOver( 163 | params: {level: number; frame: number; element: Document}, 164 | event: Event 165 | ): void { 166 | if (!this.shouldRecord(event.target as HTMLElement)) return; 167 | const {level, frame, element} = params; 168 | const currentDOMState = element.documentElement.outerHTML; 169 | if (currentDOMState === this.previousDOMState) { 170 | return; 171 | } 172 | this.previousDOMState = currentDOMState; 173 | this.handleFrameSwitches(level, frame); 174 | console.log(event, 'MouseOver'); 175 | // send mouseover event 176 | } 177 | 178 | handleChange( 179 | params: {level: number; frame: number; element: Document}, 180 | event: Event 181 | ): void { 182 | if (!this.shouldRecord(event.target as HTMLElement)) return; 183 | const {level, frame, element} = params; 184 | const waited: number = this.getWaited(); 185 | this.handleFrameSwitches(level, frame); 186 | console.log(event, 'change', (event.target as HTMLInputElement).value); 187 | const elementLocator = getPath(event.target as HTMLElement, element); 188 | // Send the keys before a cached submit statement on the same element 189 | if ( 190 | this.cachedSubmit && 191 | this.cachedSubmit.elementLocator.element !== elementLocator.element 192 | ) { 193 | // The cached submit was not on the same element, so send it 194 | this.handleCachedSubmit(); 195 | } 196 | this.sendZestScriptToZAP( 197 | new ZestStatementElementScrollTo(elementLocator, this.getWaited()), 198 | false 199 | ); 200 | this.sendZestScriptToZAP( 201 | new ZestStatementElementSendKeys( 202 | elementLocator, 203 | (event.target as HTMLInputElement).value, 204 | waited 205 | ), 206 | false 207 | ); 208 | // Now send the cached submit, if there still is one 209 | this.handleCachedSubmit(); 210 | } 211 | 212 | handleKeypress( 213 | params: {level: number; frame: number; element: Document}, 214 | event: KeyboardEvent 215 | ): void { 216 | if (!this.shouldRecord(event.target as HTMLElement)) return; 217 | const {element} = params; 218 | if (event.key === 'Enter') { 219 | if (this.cachedSubmit && this.cachedTimeStamp === event.timeStamp) { 220 | // console.log('Ignoring dup Enter event', this.cachedSubmit); 221 | return; 222 | } 223 | this.handleCachedSubmit(); 224 | const elementLocator = getPath(event.target as HTMLElement, element); 225 | // console.log('Enter key pressed', elementLocator, event.timeStamp); 226 | // Cache the statement as it often occurs before the change event occurs 227 | this.cachedSubmit = new ZestStatementElementSubmit( 228 | elementLocator, 229 | this.getWaited() 230 | ); 231 | this.cachedTimeStamp = event.timeStamp; 232 | // console.log('Caching submit', this.cachedSubmit); 233 | } 234 | } 235 | 236 | handleResize(): void { 237 | if (!this.active) return; 238 | const width = 239 | window.innerWidth || 240 | document.documentElement.clientWidth || 241 | document.body.clientWidth; 242 | const height = 243 | window.innerHeight || 244 | document.documentElement.clientHeight || 245 | document.body.clientHeight; 246 | // send window resize event 247 | console.log('Window Resize : ', width, height); 248 | } 249 | 250 | addListenerToInputField( 251 | elements: Set, 252 | inputField: HTMLElement, 253 | level: number, 254 | frame: number, 255 | element: Document 256 | ): void { 257 | if (!elements.has(inputField)) { 258 | elements.add(inputField); 259 | inputField.addEventListener( 260 | 'keydown', 261 | this.handleKeypress.bind(this, {level, frame, element}) 262 | ); 263 | } 264 | } 265 | 266 | addListenersToDocument( 267 | element: Document, 268 | level: number, 269 | frame: number 270 | ): void { 271 | // A list of all of the text elements that we have added event listeners to 272 | const textElements = new Set(); 273 | 274 | element.addEventListener( 275 | 'click', 276 | this.handleClick.bind(this, {level, frame, element}) 277 | ); 278 | element.addEventListener( 279 | 'scroll', 280 | debounce(this.handleScroll.bind(this, {level, frame, element}), 1000) 281 | ); 282 | element.addEventListener( 283 | 'mouseover', 284 | this.handleMouseOver.bind(this, {level, frame, element}) 285 | ); 286 | element.addEventListener( 287 | 'change', 288 | this.handleChange.bind(this, {level, frame, element}) 289 | ); 290 | 291 | // Add listeners to all the frames 292 | const frames = element.querySelectorAll('frame, iframe'); 293 | let i = 0; 294 | frames.forEach((_frame) => { 295 | const frameDocument = (_frame as HTMLIFrameElement | HTMLObjectElement) 296 | .contentWindow?.document; 297 | if (frameDocument != null) { 298 | this.addListenersToDocument(frameDocument, level + 1, i); 299 | i += 1; 300 | } 301 | }); 302 | 303 | // Add listeners to all of the text fields 304 | element.querySelectorAll('input').forEach((input) => { 305 | this.addListenerToInputField(textElements, input, level, frame, element); 306 | }); 307 | // Observer callback function to handle DOM mutations to detect added text fields 308 | const domMutated: MutationCallback = (mutationsList: MutationRecord[]) => { 309 | mutationsList.forEach((mutation) => { 310 | if (mutation.type === 'childList') { 311 | // Look for added input elements 312 | if (mutation.target instanceof Element) { 313 | const inputs = mutation.target.getElementsByTagName('input'); 314 | for (let j = 0; j < inputs.length; j += 1) { 315 | this.addListenerToInputField( 316 | textElements, 317 | inputs[j], 318 | level, 319 | frame, 320 | element 321 | ); 322 | } 323 | } 324 | } 325 | }); 326 | }; 327 | 328 | const observer = new MutationObserver(domMutated); 329 | observer.observe(document, { 330 | attributes: false, 331 | childList: true, 332 | subtree: true, 333 | }); 334 | } 335 | 336 | shouldRecord(element: HTMLElement): boolean { 337 | if (!this.active) return this.active; 338 | if (element.className === 'ZapfloatingDivElements') return false; 339 | return true; 340 | } 341 | 342 | getBrowserName(): string { 343 | let browserName: string; 344 | const {userAgent} = navigator; 345 | if (userAgent.includes('Chrome')) { 346 | browserName = 'chrome'; 347 | } else { 348 | browserName = 'firefox'; 349 | } 350 | return browserName; 351 | } 352 | 353 | initializationScript(loginUrl = ''): void { 354 | Browser.storage.sync.set({ 355 | initScript: false, 356 | loginUrl: '', 357 | downloadScript: true, 358 | }); 359 | const stopRecordingButton = document.getElementById(STOP_RECORDING_ID); 360 | if (stopRecordingButton) { 361 | // Can happen if recording restarted in browser launched from ZAP recorder 362 | stopRecordingButton.textContent = STOP_RECORDING_TEXT; 363 | } 364 | 365 | this.sendZestScriptToZAP( 366 | new ZestStatementComment( 367 | `Recorded by ${Browser.runtime.getManifest().name} ` + 368 | `${Browser.runtime.getManifest().version} on ${navigator.userAgent}` 369 | ) 370 | ); 371 | this.sendZestScriptToZAP( 372 | new ZestStatementLaunchBrowser( 373 | this.getBrowserName(), 374 | loginUrl !== '' ? loginUrl : window.location.href 375 | ) 376 | ); 377 | this.handleResize(); 378 | } 379 | 380 | recordUserInteractions(initScript = true, loginUrl = ''): void { 381 | console.log('user interactions'); 382 | if (initScript) { 383 | this.initializationScript(loginUrl); 384 | } 385 | this.active = true; 386 | this.previousDOMState = document.documentElement.outerHTML; 387 | if (this.haveListenersBeenAdded) { 388 | this.insertFloatingPopup(); 389 | return; 390 | } 391 | this.haveListenersBeenAdded = true; 392 | window.addEventListener('resize', debounce(this.handleResize, 100)); 393 | try { 394 | this.addListenersToDocument(document, -1, 0); 395 | } catch (err) { 396 | // Sometimes throw DOMException: Blocked a frame with current origin from accessing a cross-origin frame. 397 | console.log(err); 398 | } 399 | this.insertFloatingPopup(); 400 | } 401 | 402 | stopRecordingUserInteractions(): void { 403 | console.log('Stopping Recording User Interactions ...'); 404 | this.handleCachedSubmit(); 405 | Browser.storage.sync.set({zaprecordingactive: false}); 406 | this.active = false; 407 | const floatingDiv = document.getElementById('ZapfloatingDiv'); 408 | if (floatingDiv) { 409 | floatingDiv.style.display = 'none'; 410 | } 411 | } 412 | 413 | insertFloatingPopup(): void { 414 | if (this.floatingWindowInserted) { 415 | const floatingDiv = document.getElementById('ZapfloatingDiv'); 416 | if (floatingDiv) { 417 | floatingDiv.style.display = 'flex'; 418 | return; 419 | } 420 | } 421 | 422 | const fa = document.createElement('style'); 423 | fa.textContent = 424 | "@font-face { font-family: 'Roboto';font-style: normal;font-weight: 400;" + 425 | `src: url("${Browser.runtime.getURL( 426 | 'assets/fonts/Roboto-Regular.ttf' 427 | )}"); };`; 428 | 429 | document.head.appendChild(fa); 430 | 431 | const floatingDiv = document.createElement('div'); 432 | floatingDiv.style.all = 'initial'; 433 | floatingDiv.className = 'ZapfloatingDivElements'; 434 | floatingDiv.id = 'ZapfloatingDiv'; 435 | floatingDiv.style.position = 'fixed'; 436 | floatingDiv.style.top = '100%'; 437 | floatingDiv.style.left = '50%'; 438 | floatingDiv.style.width = '400px'; 439 | floatingDiv.style.height = '100px'; 440 | floatingDiv.style.transform = 'translate(-50%, -105%)'; 441 | floatingDiv.style.backgroundColor = '#f9f9f9'; 442 | floatingDiv.style.border = '2px solid #e74c3c'; 443 | floatingDiv.style.boxShadow = '0 2px 4px rgba(0, 0, 0, 0.1)'; 444 | floatingDiv.style.zIndex = '999999'; 445 | floatingDiv.style.textAlign = 'center'; 446 | floatingDiv.style.borderRadius = '5px'; 447 | floatingDiv.style.fontFamily = 'Roboto'; 448 | floatingDiv.style.display = 'flex'; 449 | floatingDiv.style.flexDirection = 'column'; 450 | floatingDiv.style.justifyContent = 'center'; 451 | floatingDiv.style.alignItems = 'center'; 452 | 453 | const textElement = document.createElement('p'); 454 | textElement.style.all = 'initial'; 455 | textElement.className = 'ZapfloatingDivElements'; 456 | textElement.style.margin = '0'; 457 | textElement.style.zIndex = '999999'; 458 | textElement.style.fontSize = '16px'; 459 | textElement.style.color = '#333'; 460 | textElement.style.fontFamily = 'Roboto'; 461 | textElement.textContent = 'ZAP Browser Extension is Recording...'; 462 | 463 | const buttonElement = document.createElement('button'); 464 | buttonElement.id = STOP_RECORDING_ID; 465 | buttonElement.style.all = 'initial'; 466 | buttonElement.className = 'ZapfloatingDivElements'; 467 | buttonElement.style.marginTop = '10px'; 468 | buttonElement.style.padding = '8px 15px'; 469 | buttonElement.style.background = '#e74c3c'; 470 | buttonElement.style.color = 'white'; 471 | buttonElement.style.zIndex = '999999'; 472 | buttonElement.style.border = 'none'; 473 | buttonElement.style.borderRadius = '3px'; 474 | buttonElement.style.cursor = 'pointer'; 475 | buttonElement.style.fontFamily = 'Roboto'; 476 | buttonElement.textContent = 'Stop Recording'; 477 | Browser.storage.sync 478 | .get({ 479 | downloadScript: false, 480 | }) 481 | .then((items) => { 482 | if (items.downloadScript) { 483 | buttonElement.textContent = STOP_RECORDING_TEXT; 484 | } 485 | }); 486 | 487 | buttonElement.addEventListener('click', () => { 488 | this.stopRecordingUserInteractions(); 489 | Browser.runtime.sendMessage({type: STOP_RECORDING}); 490 | Browser.storage.sync 491 | .get({ 492 | downloadScript: false, 493 | }) 494 | .then((items) => { 495 | if (items.downloadScript) { 496 | Browser.runtime.sendMessage({ 497 | type: DOWNLOAD_RECORDING, 498 | data: window.location.hostname, 499 | }); 500 | Browser.storage.sync.set({downloadScript: false}); 501 | } 502 | }); 503 | }); 504 | 505 | floatingDiv.appendChild(textElement); 506 | floatingDiv.appendChild(buttonElement); 507 | 508 | document.body.appendChild(floatingDiv); 509 | this.floatingWindowInserted = true; 510 | 511 | let isDragging = false; 512 | let initialMouseX: number; 513 | let initialMouseY: number; 514 | let initialDivX: number; 515 | let initialDivY: number; 516 | 517 | // Mouse down event listener 518 | floatingDiv.addEventListener('mousedown', (e) => { 519 | isDragging = true; 520 | initialMouseX = e.clientX; 521 | initialMouseY = e.clientY; 522 | initialDivX = floatingDiv.offsetLeft; 523 | initialDivY = floatingDiv.offsetTop; 524 | }); 525 | 526 | // Mouse move event listener 527 | window.addEventListener('mousemove', (e) => { 528 | if (!isDragging) return; 529 | 530 | const offsetX = e.clientX - initialMouseX; 531 | const offsetY = e.clientY - initialMouseY; 532 | 533 | floatingDiv.style.left = `${initialDivX + offsetX}px`; 534 | floatingDiv.style.top = `${initialDivY + offsetY}px`; 535 | }); 536 | 537 | // Mouse up event listener 538 | window.addEventListener('mouseup', () => { 539 | if (!isDragging || floatingDiv.style.left.includes('%')) { 540 | isDragging = false; 541 | return; 542 | } 543 | const width = 544 | window.innerWidth || 545 | document.documentElement.clientWidth || 546 | document.body.clientWidth; 547 | 548 | const height = 549 | window.innerHeight || 550 | document.documentElement.clientHeight || 551 | document.body.clientHeight; 552 | 553 | const leftPercent = (parseInt(floatingDiv.style.left) / width) * 100; 554 | const topPercent = (parseInt(floatingDiv.style.top) / height) * 100; 555 | 556 | floatingDiv.style.left = `${leftPercent}%`; 557 | floatingDiv.style.top = `${topPercent}%`; 558 | isDragging = false; 559 | }); 560 | } 561 | 562 | async notify(stmt: ZestStatement): Promise { 563 | const notifyMessage = { 564 | title: '', 565 | message: '', 566 | }; 567 | 568 | if (stmt instanceof ZestStatementElementClick) { 569 | notifyMessage.title = 'Click'; 570 | notifyMessage.message = stmt.elementLocator.element; 571 | } else if (stmt instanceof ZestStatementElementScrollTo) { 572 | notifyMessage.title = 'Scroll To'; 573 | notifyMessage.message = stmt.elementLocator.element; 574 | } else if (stmt instanceof ZestStatementElementSendKeys) { 575 | notifyMessage.title = 'Send Keys'; 576 | notifyMessage.message = `${stmt.elementLocator.element}: ${stmt.keys}`; 577 | } else if (stmt instanceof ZestStatementElementSubmit) { 578 | notifyMessage.title = 'Submit'; 579 | notifyMessage.message = `${stmt.elementLocator.element}`; 580 | } else if (stmt instanceof ZestStatementLaunchBrowser) { 581 | notifyMessage.title = 'Launch Browser'; 582 | notifyMessage.message = stmt.browserType; 583 | } else if (stmt instanceof ZestStatementSwitchToFrame) { 584 | notifyMessage.title = 'Switch To Frame'; 585 | notifyMessage.message = stmt.frameIndex.toString(); 586 | } 587 | 588 | // wait for previous notification to be removed 589 | if (this.isNotificationRaised) { 590 | await this.waitForNotificationToClear(); 591 | } 592 | const floatingDiv = document.getElementById('ZapfloatingDiv'); 593 | if (!floatingDiv) { 594 | console.log('Floating Div Not Found !'); 595 | return; 596 | } 597 | 598 | this.isNotificationRaised = true; 599 | const messageElement = document.createElement('p'); 600 | messageElement.className = 'ZapfloatingDivElements'; 601 | messageElement.textContent = `${notifyMessage.title}: ${notifyMessage.message}`; 602 | messageElement.style.all = 'initial'; 603 | messageElement.style.fontSize = '20px'; 604 | messageElement.style.zIndex = '999999'; 605 | messageElement.style.fontFamily = 'Roboto'; 606 | 607 | const existingChildElements = Array.from(floatingDiv.children || []); 608 | 609 | floatingDiv.innerHTML = ''; 610 | 611 | floatingDiv.appendChild(messageElement); 612 | 613 | setTimeout(() => { 614 | floatingDiv.removeChild(messageElement); 615 | existingChildElements.forEach((child) => floatingDiv.appendChild(child)); 616 | this.isNotificationRaised = false; 617 | }, 1000); 618 | } 619 | 620 | waitForNotificationToClear(): Promise { 621 | return new Promise((resolve) => { 622 | const checkInterval = setInterval(() => { 623 | if (!this.isNotificationRaised) { 624 | clearInterval(checkInterval); 625 | resolve(1); 626 | } 627 | }, 100); 628 | }); 629 | } 630 | } 631 | 632 | export default Recorder; 633 | -------------------------------------------------------------------------------- /source/ContentScript/util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import {ElementLocator} from '../types/zestScript/ZestStatement'; 21 | 22 | function isElementPathUnique(path: string, documentElement: Document): boolean { 23 | const elements = documentElement.querySelectorAll(path); 24 | return elements.length === 1; 25 | } 26 | 27 | function isElementXPathUnique( 28 | xpath: string, 29 | documentElement: Document 30 | ): boolean { 31 | const result = documentElement.evaluate( 32 | xpath, 33 | documentElement, 34 | null, 35 | XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, 36 | null 37 | ); 38 | return result.snapshotLength === 1; 39 | } 40 | 41 | function getCSSSelector( 42 | element: HTMLElement, 43 | documentElement: Document 44 | ): string { 45 | let selector = element.tagName.toLowerCase(); 46 | if (selector === 'html') { 47 | selector = 'body'; 48 | } else if (element === documentElement.body) { 49 | selector = 'body'; 50 | } else if (element.parentNode) { 51 | const parentSelector = getCSSSelector( 52 | element.parentNode as HTMLElement, 53 | documentElement 54 | ); 55 | selector = `${parentSelector} > ${selector}`; 56 | } 57 | return selector; 58 | } 59 | 60 | function getXPath(element: HTMLElement, documentElement: Document): string { 61 | if (!element.tagName) { 62 | return ''; 63 | } 64 | 65 | let selector = element.tagName.toLowerCase(); 66 | 67 | if (element.id && isElementXPathUnique(selector, documentElement)) { 68 | selector += `[@id="${element.id}"]`; 69 | } else { 70 | let index = 1; 71 | let sibling = element.previousSibling; 72 | let isUnique = true; 73 | while (sibling) { 74 | if ( 75 | sibling.nodeType === Node.ELEMENT_NODE && 76 | sibling.nodeName === element.nodeName 77 | ) { 78 | index += 1; 79 | isUnique = false; 80 | } 81 | sibling = sibling.previousSibling; 82 | } 83 | 84 | if (isUnique) { 85 | sibling = element.nextSibling; 86 | while (sibling) { 87 | if ( 88 | sibling.nodeType === Node.ELEMENT_NODE && 89 | sibling.nodeName === element.nodeName 90 | ) { 91 | isUnique = false; 92 | break; 93 | } 94 | sibling = sibling.nextSibling; 95 | } 96 | } 97 | 98 | if (index !== 1 || !isUnique) { 99 | selector += `[${index}]`; 100 | } 101 | } 102 | 103 | if (element.parentNode) { 104 | const parentSelector = getXPath( 105 | element.parentNode as HTMLElement, 106 | documentElement 107 | ); 108 | selector = `${parentSelector}/${selector}`; 109 | } 110 | return selector; 111 | } 112 | 113 | function getPath( 114 | element: HTMLElement, 115 | documentElement: Document 116 | ): ElementLocator { 117 | const path: ElementLocator = new ElementLocator('', ''); 118 | if (element.id) { 119 | path.type = 'id'; 120 | path.element = element.id; 121 | } else if ( 122 | element.classList.length === 1 && 123 | element.classList.item(0) != null && 124 | isElementPathUnique(`.${element.classList.item(0)}`, documentElement) 125 | ) { 126 | path.type = 'className'; 127 | path.element = `${element.classList.item(0)}`; 128 | } else { 129 | const selector = getCSSSelector(element, documentElement); 130 | if (selector && isElementPathUnique(selector, documentElement)) { 131 | path.type = 'cssSelector'; 132 | path.element = selector; 133 | } else { 134 | const xpath = getXPath(element, documentElement); 135 | if (xpath && isElementXPathUnique(xpath, documentElement)) { 136 | path.type = 'xpath'; 137 | path.element = xpath; 138 | } 139 | } 140 | } 141 | 142 | return path; 143 | } 144 | 145 | export {getPath}; 146 | -------------------------------------------------------------------------------- /source/Options/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import Browser from 'webextension-polyfill'; 21 | import {ZAP_ENABLE, ZAP_KEY, ZAP_URL} from '../utils/constants'; 22 | 23 | console.log('Options loading'); 24 | 25 | // Saves options to chrome.storage 26 | function saveOptions(): void { 27 | console.log('Options save_options'); 28 | const zapurl = (document.getElementById(ZAP_URL) as HTMLInputElement).value; 29 | const zapkey = (document.getElementById(ZAP_KEY) as HTMLInputElement).value; 30 | const zapenable = (document.getElementById(ZAP_ENABLE) as HTMLInputElement) 31 | .checked; 32 | const zapclosewindowhandle = ( 33 | document.getElementById('window-close-input') as HTMLInputElement 34 | ).checked; 35 | Browser.storage.sync.set({ 36 | zapurl, 37 | zapkey, 38 | zapenable, 39 | zapclosewindowhandle, 40 | }); 41 | } 42 | 43 | // Restores options from chrome.storage. 44 | function restoreOptions(): void { 45 | console.log('Options restore_options'); 46 | 47 | Browser.storage.sync 48 | .get({ 49 | zapurl: 'http://zap/', 50 | zapkey: 'not set', 51 | zapenable: false, 52 | zaprecordingactive: false, 53 | zapclosewindowhandle: true, 54 | }) 55 | .then((items) => { 56 | (document.getElementById(ZAP_URL) as HTMLInputElement).value = 57 | items.zapurl as string; 58 | (document.getElementById(ZAP_KEY) as HTMLInputElement).value = 59 | items.zapkey as string; 60 | (document.getElementById(ZAP_ENABLE) as HTMLInputElement).checked = 61 | items.zapenable as boolean; 62 | ( 63 | document.getElementById('window-close-input') as HTMLInputElement 64 | ).checked = items.zapclosewindowhandle as boolean; 65 | }); 66 | } 67 | document.addEventListener('DOMContentLoaded', restoreOptions); 68 | document.getElementById('save')?.addEventListener('click', saveOptions); 69 | 70 | export {saveOptions}; 71 | -------------------------------------------------------------------------------- /source/Options/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/fonts"; 2 | @import "../styles/reset"; 3 | @import "../styles/variables"; 4 | 5 | @import "~webext-base-css/webext-base.css"; 6 | 7 | body { 8 | color: $black; 9 | background-color: $greyWhite; 10 | } -------------------------------------------------------------------------------- /source/Popup/i18n.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import i18n from 'i18next'; 21 | import LanguageDetector from 'i18next-browser-languagedetector'; 22 | 23 | i18n.use(LanguageDetector).init({ 24 | resources: { 25 | en: { 26 | translation: { 27 | start: 'Start Recording', 28 | stop: 'Stop Recording', 29 | download: 'Download Script', 30 | options: 'Options', 31 | help: 'Help', 32 | }, 33 | }, 34 | }, 35 | defaultNS: 'translation', 36 | fallbackLng: 'en', 37 | debug: false, 38 | interpolation: { 39 | escapeValue: false, 40 | }, 41 | }); 42 | 43 | export default i18n; 44 | -------------------------------------------------------------------------------- /source/Popup/index.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import Browser from 'webextension-polyfill'; 21 | import './styles.scss'; 22 | import i18n from './i18n'; 23 | 24 | import { 25 | GET_ZEST_SCRIPT, 26 | IS_FULL_EXTENSION, 27 | RESET_ZEST_SCRIPT, 28 | SET_SAVE_SCRIPT_ENABLE, 29 | STOP_RECORDING, 30 | UPDATE_TITLE, 31 | ZAP_START_RECORDING, 32 | ZAP_STOP_RECORDING, 33 | } from '../utils/constants'; 34 | import {ZestScriptMessage} from '../types/zestScript/ZestScript'; 35 | 36 | const STOP = i18n.t('stop'); 37 | const START = i18n.t('start'); 38 | const OPTIONS = i18n.t('options'); 39 | const DOWNLOAD = i18n.t('download'); 40 | 41 | const play = document.querySelector('.play'); 42 | const pause = document.querySelector('.pause'); 43 | const wave1 = document.querySelector('.record__back-1'); 44 | const wave2 = document.querySelector('.record__back-2'); 45 | const done = document.querySelector('.done'); 46 | const optionsIcon = document.querySelector('.settings') as HTMLImageElement; 47 | const downloadIcon = document.querySelector('.download') as HTMLImageElement; 48 | 49 | const recordButton = document.getElementById('record-btn'); 50 | const configureButton = document.getElementById('configure-btn'); 51 | const helpButton = document.getElementById('help-btn'); 52 | const saveScript = document.getElementById('save-script'); 53 | const loginUrlInput = document.getElementById( 54 | 'login-url-input' 55 | ) as HTMLInputElement; 56 | const scriptNameInput = document.getElementById( 57 | 'script-name-input' 58 | ) as HTMLInputElement; 59 | const saveScriptButton = document.getElementById( 60 | 'save-script' 61 | ) as HTMLButtonElement; 62 | 63 | function sendMessageToContentScript(message: string, data = ''): void { 64 | Browser.tabs.query({active: true, currentWindow: true}).then((tabs) => { 65 | const activeTab = tabs[0]; 66 | if (activeTab?.id) { 67 | Browser.tabs.sendMessage(activeTab.id, {type: message, data}); 68 | } 69 | }); 70 | } 71 | 72 | function stoppedAnimation(): void { 73 | pause?.classList.add('visibility'); 74 | play?.classList.add('visibility'); 75 | recordButton?.classList.add('shadow'); 76 | wave1?.classList.add('paused'); 77 | wave2?.classList.add('paused'); 78 | (play as HTMLImageElement).title = START; 79 | } 80 | 81 | function startedAnimation(): void { 82 | pause?.classList.remove('visibility'); 83 | play?.classList.remove('visibility'); 84 | recordButton?.classList.remove('shadow'); 85 | wave1?.classList.remove('paused'); 86 | wave2?.classList.remove('paused'); 87 | (play as HTMLImageElement).title = STOP; 88 | } 89 | 90 | async function restoreState(): Promise { 91 | console.log('Restore state'); 92 | await Browser.runtime.sendMessage({type: SET_SAVE_SCRIPT_ENABLE}); 93 | optionsIcon.title = OPTIONS; 94 | downloadIcon.title = DOWNLOAD; 95 | Browser.storage.sync 96 | .get({ 97 | zaprecordingactive: false, 98 | zapscriptname: '', 99 | zapenablesavescript: false, 100 | }) 101 | .then((items) => { 102 | if (items.zaprecordingactive) { 103 | startedAnimation(); 104 | } else { 105 | stoppedAnimation(); 106 | } 107 | scriptNameInput.value = items.zapscriptname as string; 108 | if (items.zapclosewindowhandle) { 109 | done?.classList.remove('invisible'); 110 | } else { 111 | done?.classList.add('invisible'); 112 | } 113 | if (!items.zapenablesavescript) { 114 | saveScriptButton.classList.add('disabled'); 115 | } else { 116 | saveScriptButton.classList.remove('disabled'); 117 | } 118 | }); 119 | } 120 | 121 | function closePopup(): void { 122 | setTimeout(() => { 123 | window.close(); 124 | }, 500); 125 | } 126 | 127 | function stopRecording(): void { 128 | console.log('Recording stopped ...'); 129 | stoppedAnimation(); 130 | sendMessageToContentScript(ZAP_STOP_RECORDING); 131 | Browser.runtime.sendMessage({type: STOP_RECORDING}); 132 | Browser.storage.sync.set({ 133 | zaprecordingactive: false, 134 | }); 135 | } 136 | 137 | function startRecording(): void { 138 | startedAnimation(); 139 | Browser.storage.sync.set({ 140 | initScript: true, 141 | loginUrl: loginUrlInput.value, 142 | }); 143 | sendMessageToContentScript(ZAP_START_RECORDING); 144 | Browser.runtime.sendMessage({type: RESET_ZEST_SCRIPT}); 145 | Browser.storage.sync.set({ 146 | zaprecordingactive: true, 147 | }); 148 | } 149 | 150 | function toggleRecording(e: Event): void { 151 | e.preventDefault(); 152 | Browser.storage.sync.get({zaprecordingactive: false}).then((items) => { 153 | if (items.zaprecordingactive) { 154 | stopRecording(); 155 | console.log('active'); 156 | } else { 157 | const loginUrl = loginUrlInput.value; 158 | if (loginUrl !== '') { 159 | Browser.tabs 160 | .create({ 161 | active: true, 162 | url: loginUrl, 163 | }) 164 | .then( 165 | (_) => { 166 | startRecording(); 167 | closePopup(); 168 | }, 169 | (error) => { 170 | console.log(`Error: ${error}`); 171 | } 172 | ); 173 | } else { 174 | startRecording(); 175 | closePopup(); 176 | } 177 | } 178 | }); 179 | } 180 | 181 | function openOptionsPage(): void { 182 | Browser.tabs.create({url: 'options.html', active: true}); 183 | closePopup(); 184 | } 185 | 186 | function openHelpPage(): void { 187 | Browser.tabs.create({url: 'help.html', active: true}); 188 | closePopup(); 189 | } 190 | 191 | function downloadZestScript(zestScriptJSON: string, title: string): void { 192 | if (title === '') { 193 | scriptNameInput?.focus(); 194 | return; 195 | } 196 | const blob = new Blob([zestScriptJSON], {type: 'application/json'}); 197 | const url = URL.createObjectURL(blob); 198 | 199 | const link = document.createElement('a'); 200 | link.href = url; 201 | link.download = title + (title.slice(-4) === '.zst' ? '' : '.zst'); 202 | link.style.display = 'none'; 203 | 204 | document.body.appendChild(link); 205 | link.click(); 206 | document.body.removeChild(link); 207 | 208 | URL.revokeObjectURL(url); 209 | Browser.runtime.sendMessage({type: RESET_ZEST_SCRIPT}); 210 | Browser.storage.sync.set({ 211 | zaprecordingactive: false, 212 | }); 213 | closePopup(); 214 | } 215 | 216 | async function handleSaveScript(): Promise { 217 | const storageItems = await Browser.storage.sync.get({ 218 | zaprecordingactive: false, 219 | }); 220 | if (storageItems.zaprecordingactive) { 221 | await Browser.runtime.sendMessage({type: STOP_RECORDING}); 222 | } 223 | Browser.runtime.sendMessage({type: GET_ZEST_SCRIPT}).then((items) => { 224 | const msg = items as ZestScriptMessage; 225 | downloadZestScript(msg.script, msg.title); 226 | }); 227 | } 228 | 229 | function handleScriptNameChange(e: Event): void { 230 | const {value} = e.target as HTMLInputElement; 231 | Browser.storage.sync.set({ 232 | zapscriptname: value, 233 | }); 234 | sendMessageToContentScript(UPDATE_TITLE, value); 235 | } 236 | 237 | document.addEventListener('DOMContentLoaded', restoreState); 238 | document.addEventListener('load', restoreState); 239 | 240 | recordButton?.addEventListener('click', toggleRecording); 241 | saveScript?.addEventListener('click', handleSaveScript); 242 | scriptNameInput?.addEventListener('input', handleScriptNameChange); 243 | 244 | if (configureButton) { 245 | if (IS_FULL_EXTENSION) { 246 | configureButton.addEventListener('click', openOptionsPage); 247 | } else { 248 | configureButton.style.visibility = 'hidden'; 249 | } 250 | } 251 | if (helpButton) { 252 | helpButton.addEventListener('click', openHelpPage); 253 | } 254 | -------------------------------------------------------------------------------- /source/Popup/styles.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/fonts"; 2 | @import "../styles/variables"; 3 | @import "~webext-base-css/webext-base.css"; 4 | 5 | @font-face { 6 | font-family: 'Roboto'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: url('../assets/fonts/Roboto-Regular.ttf') format('truetype'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: normal; 15 | font-weight: 700; 16 | src: url('../assets/fonts/Roboto-Bold.ttf') format('truetype'); 17 | } 18 | 19 | body { 20 | font-family: "Roboto", sans-serif; 21 | margin: 0px; 22 | } 23 | 24 | :root { 25 | --primary-light: #F0F5FF; /* Light blue */ 26 | --primary: #2979FF; /* Vivid blue */ 27 | --primary-dark: #ff1500; /* Dark blue */ 28 | 29 | --white: #FFFFFF; /* Pure white */ 30 | --greyLight-1: #F2F2F2; /* Light grey */ 31 | --greyLight-2: #D9D9D9; /* Medium grey */ 32 | --greyLight-3: #B0B0B0; /* Dark grey */ 33 | --greyDark: #666666; /* Deeper dark grey */ 34 | --text-color: #333333; /* Dark text color */ 35 | } 36 | 37 | 38 | 39 | $shadow: .3rem .3rem .6rem var(--greyLight-2), 40 | -.2rem -.2rem .5rem var(--white); 41 | $inner-shadow: inset .2rem .2rem .5rem var(--greyLight-2), 42 | inset -.2rem -.2rem .5rem var(--white); 43 | 44 | *, *::before, *::after { 45 | margin: 0; 46 | padding: 0; 47 | box-sizing: inherit; 48 | } 49 | 50 | html { 51 | box-sizing: border-box; 52 | font-size:10px !important; 53 | overflow-y: scroll; 54 | background: var(--greyLight-1); 55 | margin: 0px; 56 | } 57 | 58 | .container { 59 | min-height: 100vh; 60 | display: flex; 61 | justify-content: center; 62 | align-items: center; 63 | background: var(--greyLight-1); 64 | } 65 | 66 | .components { 67 | width: 30rem; 68 | height: 35rem; 69 | padding: 2rem; 70 | display: grid; 71 | grid-template-columns: 0.5rem 0.5rem 0.5rem; 72 | grid-template-rows: repeat(autofit, min-content); 73 | grid-column-gap: 5rem; 74 | grid-row-gap: 2.5rem; 75 | align-items: center; 76 | } 77 | 78 | .title{ 79 | grid-column: 1/6; 80 | grid-row: 5; 81 | text-align: center; 82 | color: var(--text-color); 83 | display: flex; 84 | align-items: center; 85 | justify-content: center; 86 | } 87 | 88 | .zapicon { 89 | width: 15rem; 90 | height: 5rem; 91 | margin-right: 1rem; 92 | } 93 | 94 | /* PLAY BUTTON */ 95 | .record { 96 | grid-column: 1 / 6; 97 | grid-row: 1 / 4; 98 | width: 9rem; 99 | height: 100%; 100 | justify-self: center; 101 | border-radius: 1rem; 102 | display: grid; 103 | grid-template-rows: 1fr; 104 | justify-items: center; 105 | align-items: center; 106 | 107 | &__btn { 108 | grid-row: 1 / 2; 109 | grid-column: 1 / 2; 110 | width: 6rem; 111 | height: 6rem; 112 | display: flex; 113 | margin: .6rem; 114 | justify-content: center; 115 | align-items: center; 116 | border-radius: 50%; 117 | font-size: 3.2rem; 118 | color: var(--primary); 119 | z-index: 300; 120 | background: var(--greyLight-1); 121 | box-shadow: $shadow; 122 | cursor: pointer; 123 | position: relative; 124 | &.shadow {box-shadow: $inner-shadow;} 125 | 126 | .play { 127 | position: absolute; 128 | opacity: 0; 129 | transition: all .2s linear; 130 | &.visibility { 131 | opacity: 1; 132 | } 133 | } 134 | .pause { 135 | position: absolute; 136 | transition: all .2s linear; 137 | &.visibility { 138 | opacity: 0; 139 | } 140 | } 141 | } 142 | 143 | &__back-1, &__back-2 { 144 | grid-row: 1 / 2; 145 | grid-column: 1 / 2; 146 | width: 6rem; 147 | height: 6rem; 148 | border-radius: 50%; 149 | filter: blur(1px); 150 | z-index: 100; 151 | } 152 | 153 | &__back-1 { 154 | box-shadow: .4rem .4rem .8rem var(--greyLight-2), 155 | -.4rem -.4rem .8rem var(--white); 156 | background: linear-gradient(to bottom right, var(--greyLight-2) 0%, var(--white) 100%); 157 | animation: waves 4s linear infinite; 158 | 159 | &.paused { 160 | animation-play-state: paused; 161 | } 162 | } 163 | 164 | &__back-2 { 165 | box-shadow: .4rem .4rem .8rem var(--greyLight-2), 166 | -.4rem -.4rem .8rem var(--white); 167 | animation: waves 4s linear 2s infinite; 168 | 169 | &.paused { 170 | animation-play-state: paused; 171 | } 172 | } 173 | } 174 | 175 | /* FORM */ 176 | .form { 177 | grid-column: 1 / 6; 178 | grid-row: 4 / 5 ; 179 | align-self: center; 180 | display: flex; 181 | flex-wrap: wrap; 182 | justify-content:space-between; 183 | color: var(--text-color); 184 | 185 | &__input { 186 | width: 20.4rem; 187 | height: 4rem; 188 | border: none; 189 | border-radius: 1rem; 190 | font-size: 1.4rem; 191 | padding-left: 1.4rem; 192 | box-shadow: $inner-shadow; 193 | background: none; 194 | font-family: inherit; 195 | 196 | &.flex1 { 197 | flex: 1; 198 | } 199 | 200 | &::placeholder { color: var(--greyLight-3); } 201 | &:focus { outline: none; box-shadow: $shadow; } 202 | &:focus-visible { 203 | outline: var(--text-color); 204 | box-shadow: $shadow; 205 | } 206 | } 207 | } 208 | 209 | /* ICONS */ 210 | .setting-icon { 211 | grid-column: 1; 212 | grid-row: 1 ; 213 | &__settings { 214 | width: 4rem; 215 | height: 4rem; 216 | border-radius: 50%; 217 | box-shadow: $shadow; 218 | display: flex; 219 | justify-content: center; 220 | align-items: center; 221 | font-size: 2rem; 222 | cursor: pointer; 223 | color: var(--greyDark); 224 | transition: all .5s ease; 225 | 226 | &:active { 227 | box-shadow: $inner-shadow; 228 | color: var(--primary); 229 | } 230 | &:hover {color: var(--primary);} 231 | } 232 | } 233 | 234 | .help-icon { 235 | grid-column: 5; 236 | grid-row: 1; 237 | &__settings { 238 | width: 4rem; 239 | height: 4rem; 240 | border-radius: 50%; 241 | box-shadow: $shadow; 242 | display: flex; 243 | justify-content: center; 244 | align-items: center; 245 | font-size: 2rem; 246 | cursor: pointer; 247 | color: var(--greyDark); 248 | transition: all .5s ease; 249 | 250 | &:active { 251 | box-shadow: $inner-shadow; 252 | color: var(--primary); 253 | } 254 | &:hover {color: var(--primary);} 255 | } 256 | } 257 | 258 | /* ICONS */ 259 | 260 | .download-icon { 261 | width: 4rem; 262 | height: 4rem; 263 | border-radius: 50%; 264 | box-shadow: $shadow; 265 | display: flex; 266 | justify-content: center; 267 | align-items: center; 268 | font-size: 2rem; 269 | cursor: pointer; 270 | color: var(--greyDark); 271 | transition: all .5s ease; 272 | 273 | &:active { 274 | box-shadow: $inner-shadow; 275 | color: var(--primary); 276 | } 277 | &:hover {color: var(--primary);} 278 | 279 | &.disabled { 280 | pointer-events: none; 281 | opacity: 0.5; 282 | } 283 | } 284 | 285 | @keyframes waves { 286 | 0% { 287 | transform: scale(1); 288 | opacity: 1; 289 | } 290 | 291 | 50% { 292 | opacity: 1; 293 | } 294 | 295 | 100% { 296 | transform: scale(2); 297 | opacity: 0; 298 | } 299 | } 300 | 301 | .pause, .play{ 302 | width: 3rem; 303 | } 304 | 305 | .download, .settings, .help, .done{ 306 | width: 2rem; 307 | } -------------------------------------------------------------------------------- /source/assets/fonts/Roboto-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/fonts/Roboto-Bold.ttf -------------------------------------------------------------------------------- /source/assets/fonts/Roboto-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/fonts/Roboto-Regular.ttf -------------------------------------------------------------------------------- /source/assets/icons/ZAP_by_Checkmarx_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/ZAP_by_Checkmarx_logo.png -------------------------------------------------------------------------------- /source/assets/icons/done.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/download.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /source/assets/icons/help.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 10 | 11 | 22 | 24 | 25 | -------------------------------------------------------------------------------- /source/assets/icons/pause.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/play.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/radio-button-on-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/stop-circle-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /source/assets/icons/zap128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap128x128.png -------------------------------------------------------------------------------- /source/assets/icons/zap16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap16x16.png -------------------------------------------------------------------------------- /source/assets/icons/zap256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap256x256.png -------------------------------------------------------------------------------- /source/assets/icons/zap32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap32x32.png -------------------------------------------------------------------------------- /source/assets/icons/zap48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap48x48.png -------------------------------------------------------------------------------- /source/assets/icons/zap512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/zaproxy/browser-extension/8374a0538d4885fb0841495fcb1abe5faed647a2/source/assets/icons/zap512x512.png -------------------------------------------------------------------------------- /source/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ZAP by Checkmarx Browser Extension", 4 | "version": "0.1.0", 5 | 6 | "icons": { 7 | "16": "assets/icons/zap16x16.png", 8 | "32": "assets/icons/zap32x32.png", 9 | "48": "assets/icons/zap48x48.png", 10 | "128": "assets/icons/zap128x128.png", 11 | "512": "assets/icons/zap512x512.png" 12 | }, 13 | "description": "A browser extension which allows ZAP to access the client side of a web app.", 14 | "homepage_url": "https://github.com/zaproxy/browser-extension/", 15 | "short_name": "ZAP by Checkmarx Extension", 16 | "incognito": "spanning", 17 | 18 | "permissions": [ 19 | "tabs", 20 | "cookies", 21 | "storage" 22 | ], 23 | 24 | "host_permissions": [ 25 | "http://*/*", 26 | "https://*/*" 27 | ], 28 | 29 | "content_security_policy": { 30 | "extension_pages" : "script-src 'self'; object-src 'self'" 31 | }, 32 | 33 | "__chrome|firefox__author": "ZAP Core Team", 34 | "__opera__developer": { 35 | "name": "ZAP Core Team" 36 | }, 37 | 38 | "__firefox__browser_specific_settings": { 39 | "gecko": { 40 | "id": "browser-extensionV3@zaproxy.org" 41 | } 42 | }, 43 | 44 | "__chrome__minimum_chrome_version": "113", 45 | "__opera__minimum_opera_version": "36", 46 | 47 | "action": { 48 | "default_icon": { 49 | "16": "assets/icons/zap16x16.png", 50 | "32": "assets/icons/zap32x32.png", 51 | "48": "assets/icons/zap48x48.png", 52 | "128": "assets/icons/zap128x128.png", 53 | "512": "assets/icons/zap512x512.png" 54 | }, 55 | "default_title": "ZAP by Checkmarx", 56 | "default_popup": "popup.html", 57 | "__chrome|opera__chrome_style": false, 58 | "__firefox__browser_style": false 59 | }, 60 | 61 | "__chrome|opera__options_page": "options.html", 62 | "options_ui": { 63 | "page": "options.html", 64 | "open_in_tab": true 65 | }, 66 | 67 | "background": { 68 | "__chrome__service_worker": "js/background.bundle.js", 69 | "__firefox|opera__scripts": [ 70 | "js/background.bundle.js" 71 | ] 72 | }, 73 | 74 | "content_scripts": [{ 75 | "matches": [ 76 | "http://*/*", 77 | "https://*/*" 78 | ], 79 | "js": [ 80 | "js/contentScript.bundle.js" 81 | ] 82 | }], 83 | 84 | "web_accessible_resources": [ 85 | { 86 | "resources": [ "assets/fonts/Roboto-Regular.ttf"], 87 | "matches": [ 88 | "http://*/*", 89 | "https://*/*" 90 | ] 91 | } 92 | ] 93 | } -------------------------------------------------------------------------------- /source/manifest.rec.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "ZAP by Checkmarx Recorder", 4 | "version": "0.1.0", 5 | 6 | "icons": { 7 | "16": "assets/icons/zap16x16.png", 8 | "32": "assets/icons/zap32x32.png", 9 | "48": "assets/icons/zap48x48.png", 10 | "128": "assets/icons/zap128x128.png", 11 | "512": "assets/icons/zap512x512.png" 12 | }, 13 | "description": "An extension for recording actions taken in the browser. It can be used to record things like auth scripts to be used in ZAP.", 14 | "homepage_url": "https://github.com/zaproxy/browser-extension/", 15 | "short_name": "ZAP Recorder", 16 | "incognito": "spanning", 17 | 18 | "permissions": [ 19 | "storage" 20 | ], 21 | 22 | "host_permissions": [ 23 | "http://*/*", 24 | "https://*/*" 25 | ], 26 | 27 | "content_security_policy": { 28 | "extension_pages" : "script-src 'self'; object-src 'self'" 29 | }, 30 | 31 | "__chrome|firefox__author": "ZAP Core Team", 32 | "__opera__developer": { 33 | "name": "ZAP Core Team" 34 | }, 35 | 36 | "__firefox__browser_specific_settings": { 37 | "gecko": { 38 | "id": "browser-recorder@zaproxy.org" 39 | } 40 | }, 41 | 42 | "__chrome__minimum_chrome_version": "113", 43 | "__opera__minimum_opera_version": "36", 44 | 45 | "action": { 46 | "default_icon": { 47 | "16": "assets/icons/zap16x16.png", 48 | "32": "assets/icons/zap32x32.png", 49 | "48": "assets/icons/zap48x48.png", 50 | "128": "assets/icons/zap128x128.png", 51 | "512": "assets/icons/zap512x512.png" 52 | }, 53 | "default_title": "ZAP by Checkmarx Recorder", 54 | "default_popup": "popup.html", 55 | "__chrome|opera__chrome_style": false, 56 | "__firefox__browser_style": false 57 | }, 58 | 59 | "background": { 60 | "__chrome__service_worker": "js/background.bundle.js", 61 | "__firefox|opera__scripts": [ 62 | "js/background.bundle.js" 63 | ] 64 | }, 65 | 66 | "content_scripts": [{ 67 | "matches": [ 68 | "http://*/*", 69 | "https://*/*" 70 | ], 71 | "js": [ 72 | "js/contentScript.bundle.js" 73 | ] 74 | }], 75 | 76 | "web_accessible_resources": [ 77 | { 78 | "resources": [ "assets/fonts/Roboto-Regular.ttf"], 79 | "matches": [ 80 | "http://*/*", 81 | "https://*/*" 82 | ] 83 | } 84 | ] 85 | } -------------------------------------------------------------------------------- /source/styles/_fonts.scss: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css?family=Nunito:400,600"); 2 | -------------------------------------------------------------------------------- /source/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | @import '~advanced-css-reset/dist/reset.css'; 2 | 3 | // Add your custom reset rules here 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | border: 0; 9 | outline: 0; 10 | } -------------------------------------------------------------------------------- /source/styles/_variables.scss: -------------------------------------------------------------------------------- 1 | // colors 2 | $black: #0d0d0d; 3 | $greyWhite: #f3f3f3; 4 | $skyBlue: #8892b0; 5 | 6 | // fonts 7 | $nunito: "Nunito", sans-serif; 8 | 9 | // font weights 10 | $thin: 100; 11 | $exlight: 200; 12 | $light: 300; 13 | $regular: 400; 14 | $medium: 500; 15 | $semibold: 600; 16 | $bold: 700; 17 | $exbold: 800; 18 | $exblack: 900; 19 | 20 | // other variables 21 | .d-none { 22 | display: none !important; 23 | } 24 | -------------------------------------------------------------------------------- /source/types/ReportedModel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | class ReportedObject { 21 | public timestamp: number; 22 | 23 | public type: string; 24 | 25 | public tagName: string; 26 | 27 | public id: string; 28 | 29 | public nodeName: string; 30 | 31 | public url: string; 32 | 33 | public xpath: string; 34 | 35 | public href: string | null; 36 | 37 | public text: string | null; 38 | 39 | public constructor( 40 | type: string, 41 | tagName: string, 42 | id: string, 43 | nodeName: string, 44 | text: string | null, 45 | url: string 46 | ) { 47 | this.timestamp = Date.now(); 48 | this.type = type; 49 | this.tagName = tagName; 50 | this.id = id; 51 | this.nodeName = nodeName; 52 | this.text = text; 53 | this.url = url; 54 | } 55 | 56 | public toString(): string { 57 | return JSON.stringify(this); 58 | } 59 | 60 | public toShortString(): string { 61 | return JSON.stringify(this, function replacer(k: string, v: string) { 62 | if (k === 'xpath') { 63 | // Dont return the xpath value - it can change too often in many cases 64 | return undefined; 65 | } 66 | return v; 67 | }); 68 | } 69 | 70 | // Use this for tests 71 | public toNonTimestampString(): string { 72 | return JSON.stringify(this, function replacer(k: string, v: string) { 73 | if (k === 'timestamp') { 74 | return undefined; 75 | } 76 | return v; 77 | }); 78 | } 79 | } 80 | 81 | class ReportedStorage extends ReportedObject { 82 | public toShortString(): string { 83 | return JSON.stringify(this, function replacer(k: string, v: string) { 84 | if ( 85 | k === 'xpath' || 86 | k === 'href' || 87 | k === 'timestamp' || 88 | (k === 'url' && !(this.type === 'cookies')) 89 | ) { 90 | // Storage events are not time or URL specific 91 | return undefined; 92 | } 93 | return v; 94 | }); 95 | } 96 | } 97 | 98 | class ReportedElement extends ReportedObject { 99 | public tagType: string | null; 100 | 101 | public formId: number | null; 102 | 103 | public constructor(element: Element, url: string) { 104 | super( 105 | 'nodeAdded', 106 | element.tagName, 107 | element.id, 108 | element.nodeName, 109 | element.textContent, 110 | url 111 | ); 112 | if (element.tagName === 'A') { 113 | // This gets the full URL rather than a relative one 114 | const a: HTMLAnchorElement = element as HTMLAnchorElement; 115 | this.href = a.toString(); 116 | } else if (element.tagName === 'FORM') { 117 | this.formId = Array.prototype.slice.call(document.forms).indexOf(element); 118 | } else if (element.tagName === 'INPUT') { 119 | // Capture extra useful info for input elements 120 | const input: HTMLInputElement = element as HTMLInputElement; 121 | this.tagType = input.type; 122 | this.text = input.value; 123 | const {form} = input; 124 | if (form) { 125 | // This will not work if form tags are not used 126 | this.formId = Array.prototype.slice.call(document.forms).indexOf(form); 127 | } 128 | } else if (element.hasAttribute('href')) { 129 | this.href = element.getAttribute('href'); 130 | } 131 | } 132 | 133 | public toShortString(): string { 134 | return JSON.stringify(this, function replacer(k: string, v: string) { 135 | if (k === 'timestamp') { 136 | // No point reporting the same element lots of times 137 | return undefined; 138 | } 139 | return v; 140 | }); 141 | } 142 | } 143 | 144 | class ReportedEvent { 145 | public timestamp: number; 146 | 147 | public eventName: string; 148 | 149 | public url: string; 150 | 151 | public count: number; 152 | 153 | public constructor(eventName: string) { 154 | this.timestamp = Date.now(); 155 | this.eventName = eventName; 156 | this.url = window.location.href; 157 | this.count = 1; 158 | } 159 | 160 | public toString(): string { 161 | return JSON.stringify(this); 162 | } 163 | } 164 | 165 | export {ReportedElement, ReportedObject, ReportedStorage, ReportedEvent}; 166 | -------------------------------------------------------------------------------- /source/types/zestScript/ZestScript.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import Browser from 'webextension-polyfill'; 21 | 22 | interface ZestScriptMessage { 23 | script: string; 24 | title: string; 25 | } 26 | 27 | class ZestScript { 28 | private zestStatements: string[] = []; 29 | 30 | private curIndex = 1; 31 | 32 | private title: string; 33 | 34 | constructor(title = '') { 35 | this.title = title; 36 | } 37 | 38 | addStatement(statement: string): string { 39 | const zestStatement = JSON.parse(statement); 40 | zestStatement.index = this.getNextIndex(); 41 | this.zestStatements.push(JSON.stringify(zestStatement)); 42 | return JSON.stringify(zestStatement); 43 | } 44 | 45 | getNextIndex(): number { 46 | this.curIndex += 1; 47 | return this.curIndex - 1; 48 | } 49 | 50 | reset(): void { 51 | this.zestStatements = []; 52 | this.curIndex = 1; 53 | } 54 | 55 | getZestStatementCount(): number { 56 | return this.zestStatements.length; 57 | } 58 | 59 | getTitle(): string { 60 | return this.title; 61 | } 62 | 63 | toJSON(): string { 64 | return JSON.stringify( 65 | { 66 | about: 67 | 'This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/', 68 | zestVersion: '0.3', 69 | title: this.title, 70 | description: '', 71 | prefix: '', 72 | type: 'StandAlone', 73 | parameters: { 74 | tokenStart: '{{', 75 | tokenEnd: '}}', 76 | tokens: {}, 77 | elementType: 'ZestVariables', 78 | }, 79 | statements: this.zestStatements.map((statement) => 80 | JSON.parse(statement) 81 | ), 82 | authentication: [], 83 | index: 0, 84 | enabled: true, 85 | elementType: 'ZestScript', 86 | }, 87 | null, 88 | 2 89 | ); 90 | } 91 | 92 | getZestScript(): Promise { 93 | return new Promise((resolve) => { 94 | Browser.storage.sync.get({zapscriptname: this.title}).then((items) => { 95 | this.title = items.zapscriptname as string; 96 | resolve({script: this.toJSON(), title: this.title}); 97 | }); 98 | }); 99 | } 100 | } 101 | 102 | export {ZestScript}; 103 | export type {ZestScriptMessage}; 104 | -------------------------------------------------------------------------------- /source/types/zestScript/ZestStatement.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import { 21 | DEFAULT_WINDOW_HANDLE, 22 | ZEST_CLIENT_ELEMENT_CLEAR, 23 | ZEST_CLIENT_ELEMENT_CLICK, 24 | ZEST_CLIENT_ELEMENT_MOUSE_OVER, 25 | ZEST_CLIENT_ELEMENT_SCROLL_TO, 26 | ZEST_CLIENT_ELEMENT_SEND_KEYS, 27 | ZEST_CLIENT_ELEMENT_SUBMIT, 28 | ZEST_CLIENT_LAUNCH, 29 | ZEST_CLIENT_SWITCH_TO_FRAME, 30 | ZEST_CLIENT_WINDOW_CLOSE, 31 | ZEST_COMMENT, 32 | } from '../../utils/constants'; 33 | 34 | class ElementLocator { 35 | type: string; 36 | 37 | element: string; 38 | 39 | constructor(type: string, element: string) { 40 | this.type = type; 41 | this.element = element; 42 | } 43 | 44 | toJSON(): {type: string; element: string} { 45 | return { 46 | type: this.type, 47 | element: this.element, 48 | }; 49 | } 50 | } 51 | 52 | abstract class ZestStatement { 53 | index: number; 54 | 55 | elementType: string; 56 | 57 | constructor(elementType: string) { 58 | this.elementType = elementType; 59 | this.index = -1; 60 | } 61 | 62 | abstract toJSON(): string; 63 | } 64 | 65 | class ZestStatementLaunchBrowser extends ZestStatement { 66 | windowHandle: string; 67 | 68 | browserType: string; 69 | 70 | url: string; 71 | 72 | constructor( 73 | browserType: string, 74 | url: string, 75 | windowHandle = DEFAULT_WINDOW_HANDLE 76 | ) { 77 | super(ZEST_CLIENT_LAUNCH); 78 | this.windowHandle = windowHandle; 79 | this.browserType = browserType; 80 | this.url = url; 81 | } 82 | 83 | toJSON(): string { 84 | return JSON.stringify({ 85 | windowHandle: this.windowHandle, 86 | browserType: this.browserType, 87 | url: this.url, 88 | capabilities: '', 89 | headless: false, 90 | index: this.index, 91 | enabled: true, 92 | elementType: this.elementType, 93 | }); 94 | } 95 | } 96 | 97 | class ZestStatementComment extends ZestStatement { 98 | comment: string; 99 | 100 | constructor(comment: string) { 101 | super(ZEST_COMMENT); 102 | this.comment = comment; 103 | } 104 | 105 | toJSON(): string { 106 | return JSON.stringify({ 107 | index: this.index, 108 | enabled: true, 109 | elementType: this.elementType, 110 | comment: this.comment, 111 | }); 112 | } 113 | } 114 | 115 | abstract class ZestStatementElement extends ZestStatement { 116 | elementLocator: ElementLocator; 117 | 118 | windowHandle: string; 119 | 120 | constructor(elementType: string, elementLocator: ElementLocator) { 121 | super(elementType); 122 | this.elementLocator = elementLocator; 123 | } 124 | } 125 | 126 | class ZestStatementElementClick extends ZestStatementElement { 127 | waitForMsec: number; 128 | 129 | constructor( 130 | elementLocator: ElementLocator, 131 | waitForMsec: number, 132 | windowHandle = DEFAULT_WINDOW_HANDLE 133 | ) { 134 | super(ZEST_CLIENT_ELEMENT_CLICK, elementLocator); 135 | this.windowHandle = windowHandle; 136 | this.waitForMsec = waitForMsec; 137 | } 138 | 139 | toJSON(): string { 140 | return JSON.stringify({ 141 | windowHandle: this.windowHandle, 142 | ...this.elementLocator.toJSON(), 143 | index: this.index, 144 | waitForMsec: this.waitForMsec, 145 | enabled: true, 146 | elementType: this.elementType, 147 | }); 148 | } 149 | } 150 | 151 | class ZestStatementElementScrollTo extends ZestStatementElement { 152 | waitForMsec: number; 153 | 154 | constructor( 155 | elementLocator: ElementLocator, 156 | waitForMsec: number, 157 | windowHandle = DEFAULT_WINDOW_HANDLE 158 | ) { 159 | super(ZEST_CLIENT_ELEMENT_SCROLL_TO, elementLocator); 160 | this.waitForMsec = waitForMsec; 161 | this.windowHandle = windowHandle; 162 | } 163 | 164 | toJSON(): string { 165 | return JSON.stringify({ 166 | windowHandle: this.windowHandle, 167 | ...this.elementLocator.toJSON(), 168 | index: this.index, 169 | waitForMsec: this.waitForMsec, 170 | enabled: true, 171 | elementType: this.elementType, 172 | }); 173 | } 174 | } 175 | 176 | class ZestStatementElementSendKeys extends ZestStatementElement { 177 | keys: string; 178 | 179 | waitForMsec: number; 180 | 181 | constructor( 182 | elementLocator: ElementLocator, 183 | keys: string, 184 | waitForMsec: number, 185 | windowHandle = DEFAULT_WINDOW_HANDLE 186 | ) { 187 | super(ZEST_CLIENT_ELEMENT_SEND_KEYS, elementLocator); 188 | this.keys = keys; 189 | this.waitForMsec = waitForMsec; 190 | this.windowHandle = windowHandle; 191 | } 192 | 193 | toJSON(): string { 194 | return JSON.stringify({ 195 | value: this.keys, 196 | windowHandle: this.windowHandle, 197 | ...this.elementLocator.toJSON(), 198 | index: this.index, 199 | waitForMsec: this.waitForMsec, 200 | enabled: true, 201 | elementType: this.elementType, 202 | }); 203 | } 204 | } 205 | 206 | class ZestStatementElementSubmit extends ZestStatementElement { 207 | keys: string; 208 | 209 | waitForMsec: number; 210 | 211 | constructor( 212 | elementLocator: ElementLocator, 213 | waitForMsec: number, 214 | windowHandle = DEFAULT_WINDOW_HANDLE 215 | ) { 216 | super(ZEST_CLIENT_ELEMENT_SUBMIT, elementLocator); 217 | this.waitForMsec = waitForMsec; 218 | this.windowHandle = windowHandle; 219 | } 220 | 221 | toJSON(): string { 222 | return JSON.stringify({ 223 | value: this.keys, 224 | windowHandle: this.windowHandle, 225 | ...this.elementLocator.toJSON(), 226 | index: this.index, 227 | waitForMsec: this.waitForMsec, 228 | enabled: true, 229 | elementType: this.elementType, 230 | }); 231 | } 232 | } 233 | 234 | class ZestStatementElementClear extends ZestStatementElement { 235 | constructor( 236 | elementLocator: ElementLocator, 237 | windowHandle = DEFAULT_WINDOW_HANDLE 238 | ) { 239 | super(ZEST_CLIENT_ELEMENT_CLEAR, elementLocator); 240 | this.windowHandle = windowHandle; 241 | } 242 | 243 | toJSON(): string { 244 | return JSON.stringify({ 245 | windowHandle: this.windowHandle, 246 | ...this.elementLocator.toJSON(), 247 | index: this.index, 248 | enabled: true, 249 | elementType: this.elementType, 250 | }); 251 | } 252 | } 253 | 254 | class ZestStatementWindowClose extends ZestStatement { 255 | sleepInSeconds: number; 256 | 257 | windowHandle: string; 258 | 259 | constructor(sleepInSeconds: number, windowHandle = DEFAULT_WINDOW_HANDLE) { 260 | super(ZEST_CLIENT_WINDOW_CLOSE); 261 | this.sleepInSeconds = sleepInSeconds; 262 | this.windowHandle = windowHandle; 263 | } 264 | 265 | toJSON(): string { 266 | return JSON.stringify({ 267 | windowHandle: this.windowHandle, 268 | index: this.index, 269 | sleepInSeconds: this.sleepInSeconds, 270 | enabled: true, 271 | elementType: this.elementType, 272 | }); 273 | } 274 | } 275 | 276 | class ZestStatementSwitchToFrame extends ZestStatement { 277 | frameIndex: number; 278 | 279 | frameName: string; 280 | 281 | windowHandle: string; 282 | 283 | constructor( 284 | frameIndex: number, 285 | frameName = '', 286 | windowHandle = DEFAULT_WINDOW_HANDLE 287 | ) { 288 | super(ZEST_CLIENT_SWITCH_TO_FRAME); 289 | this.frameIndex = frameIndex; 290 | this.frameName = frameName; 291 | this.windowHandle = windowHandle; 292 | } 293 | 294 | toJSON(): string { 295 | return JSON.stringify({ 296 | windowHandle: this.windowHandle, 297 | frameIndex: this.frameIndex, 298 | frameName: this.frameName, 299 | parent: this.frameIndex === -1, 300 | index: this.index, 301 | enabled: true, 302 | elementType: this.elementType, 303 | }); 304 | } 305 | } 306 | 307 | class ZestStatementElementMouseOver extends ZestStatementElement { 308 | constructor(elementLocator: ElementLocator) { 309 | super(ZEST_CLIENT_ELEMENT_MOUSE_OVER, elementLocator); 310 | } 311 | 312 | toJSON(): string { 313 | return JSON.stringify({ 314 | elementType: this.elementType, 315 | }); 316 | } 317 | } 318 | 319 | export { 320 | ElementLocator, 321 | ZestStatement, 322 | ZestStatementComment, 323 | ZestStatementLaunchBrowser, 324 | ZestStatementElementMouseOver, 325 | ZestStatementElementClick, 326 | ZestStatementSwitchToFrame, 327 | ZestStatementElementScrollTo, 328 | ZestStatementElementSendKeys, 329 | ZestStatementElementSubmit, 330 | ZestStatementElementClear, 331 | ZestStatementWindowClose, 332 | }; 333 | -------------------------------------------------------------------------------- /source/utils/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import Browser from 'webextension-polyfill'; 21 | 22 | // The manifest will be undefined when running the tests 23 | export const IS_FULL_EXTENSION = 24 | typeof Browser.runtime.getManifest === 'undefined' || 25 | Browser.runtime.getManifest().name === 'ZAP by Checkmarx Browser Extension'; 26 | 27 | export const ZEST_CLIENT_SWITCH_TO_FRAME = 'ZestClientSwitchToFrame'; 28 | export const ZEST_CLIENT_ELEMENT_CLICK = 'ZestClientElementClick'; 29 | export const ZEST_CLIENT_ELEMENT_SCROLL_TO = 'ZestClientElementScrollTo'; 30 | export const ZEST_CLIENT_ELEMENT_SEND_KEYS = 'ZestClientElementSendKeys'; 31 | export const ZEST_CLIENT_ELEMENT_SUBMIT = 'ZestClientElementSubmit'; 32 | export const ZEST_CLIENT_LAUNCH = 'ZestClientLaunch'; 33 | export const ZEST_CLIENT_ELEMENT_CLEAR = 'ZestClientElementClear'; 34 | export const ZEST_CLIENT_WINDOW_CLOSE = 'ZestClientWindowClose'; 35 | export const ZEST_CLIENT_ELEMENT_MOUSE_OVER = 'ZestClientElementMouseOver'; 36 | export const ZEST_COMMENT = 'ZestComment'; 37 | export const DEFAULT_WINDOW_HANDLE = 'windowHandle1'; 38 | 39 | export const ZAP_STOP_RECORDING = 'zapStopRecording'; 40 | export const ZAP_START_RECORDING = 'zapStartRecording'; 41 | export const SET_SAVE_SCRIPT_ENABLE = 'setSaveScriptEnable'; 42 | export const ZEST_SCRIPT = 'zestScript'; 43 | 44 | export const DOWNLOAD_RECORDING = 'downloadRecording'; 45 | export const STOP_RECORDING = 'stopRecording'; 46 | export const RESET_ZEST_SCRIPT = 'resetZestScript'; 47 | export const GET_ZEST_SCRIPT = 'getZestScript'; 48 | export const UPDATE_TITLE = 'updateTitle'; 49 | 50 | export const REPORT_EVENT = 'reportEvent'; 51 | export const REPORT_OBJECT = 'reportObject'; 52 | 53 | export const LOCAL_STORAGE = 'localStorage'; 54 | export const SESSION_STORAGE = 'sessionStorage'; 55 | export const LOCAL_ZAP_URL = 'localzapurl'; 56 | export const LOCAL_ZAP_ENABLE = 'localzapenable'; 57 | export const LOCAL_ZAP_RECORD = 'localzaprecord'; 58 | export const URL_ZAP_ENABLE = 'zapenable'; 59 | export const URL_ZAP_RECORD = 'zaprecord'; 60 | 61 | export const ZAP_URL = 'zapurl'; 62 | export const ZAP_KEY = 'zapkey'; 63 | export const ZAP_ENABLE = 'zapenable'; 64 | -------------------------------------------------------------------------------- /test/Background/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "outDir": "../dist" 7 | }, 8 | "include": [ 9 | "**/*.test.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /test/Background/unitTests.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | /* 5 | * Zed Attack Proxy (ZAP) and its related source files. 6 | * 7 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 8 | * 9 | * Copyright 2023 The ZAP Development Team 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | import {TextEncoder, TextDecoder} from 'util'; 24 | import * as src from '../../source/Background/index'; 25 | 26 | console.log(src); 27 | console.log(TextEncoder); 28 | console.log(TextDecoder); 29 | 30 | jest.mock('webextension-polyfill'); 31 | 32 | // These lines must appear before the JSDOM import 33 | global.TextEncoder = TextEncoder; 34 | global.TextDecoder = TextDecoder as typeof global.TextDecoder; 35 | 36 | // eslint-disable-next-line import/order,import/first 37 | import Browser from 'webextension-polyfill'; 38 | 39 | test('Report storage', () => { 40 | // Given 41 | 42 | const setCookie = Browser.cookies.set({ 43 | url: 'https://www.example.com/', 44 | name: 'ZAP', 45 | value: 'Proxy', 46 | domain: 'example.com', 47 | path: '/', 48 | }); 49 | 50 | setCookie.then((newCookie) => { 51 | console.log(newCookie); 52 | // When 53 | const success = src.reportCookies( 54 | newCookie, 55 | 'http://localhost:8080/', 56 | 'secretKey' 57 | ); 58 | // Then 59 | expect(success).toBe(true); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /test/ContentScript/constants.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import path from 'path'; 21 | 22 | export const extensionPath = { 23 | CHROME: path.join(__dirname, '..', '..', 'extension', 'chrome'), 24 | FIREFOX: path.join(__dirname, '..', '..', 'extension', 'firefox.xpi'), 25 | }; 26 | 27 | export const HTTPPORT = 1801; 28 | export const JSONPORT = 8080; 29 | export const BROWSERNAME = { 30 | CHROME: 'chrome', 31 | FIREFOX: 'firefox', 32 | }; 33 | -------------------------------------------------------------------------------- /test/ContentScript/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "esModuleInterop": true, 5 | "module": "commonjs", 6 | "outDir": "../dist" 7 | }, 8 | "include": [ 9 | "**/*.test.ts" 10 | ] 11 | } -------------------------------------------------------------------------------- /test/ContentScript/unitTests.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | /* 5 | * Zed Attack Proxy (ZAP) and its related source files. 6 | * 7 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 8 | * 9 | * Copyright 2023 The ZAP Development Team 10 | * 11 | * Licensed under the Apache License, Version 2.0 (the "License"); 12 | * you may not use this file except in compliance with the License. 13 | * You may obtain a copy of the License at 14 | * 15 | * http://www.apache.org/licenses/LICENSE-2.0 16 | * 17 | * Unless required by applicable law or agreed to in writing, software 18 | * distributed under the License is distributed on an "AS IS" BASIS, 19 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 20 | * See the License for the specific language governing permissions and 21 | * limitations under the License. 22 | */ 23 | import {TextEncoder, TextDecoder} from 'util'; 24 | import * as src from '../../source/ContentScript/index'; 25 | import {ZestScript} from '../../source/types/zestScript/ZestScript'; 26 | import {getPath} from '../../source/ContentScript/util'; 27 | import { 28 | ElementLocator, 29 | ZestStatementElementClick, 30 | ZestStatementElementSendKeys, 31 | ZestStatementSwitchToFrame, 32 | } from '../../source/types/zestScript/ZestStatement'; 33 | 34 | jest.mock('webextension-polyfill'); 35 | 36 | // These lines must appear before the JSDOM import 37 | global.TextEncoder = TextEncoder; 38 | global.TextDecoder = TextDecoder as typeof global.TextDecoder; 39 | 40 | // eslint-disable-next-line import/order,import/first 41 | import {JSDOM} from 'jsdom'; 42 | 43 | test('ReportedObject toString as expected', () => { 44 | // Given / When 45 | const ro: src.ReportedObject = new src.ReportedObject( 46 | 'a', 47 | 'b', 48 | 'c', 49 | 'd', 50 | 'e', 51 | 'http://localhost/' 52 | ); 53 | 54 | // Then 55 | expect(ro.toNonTimestampString()).toBe( 56 | '{"type":"a","tagName":"b","id":"c","nodeName":"d","url":"http://localhost/","text":"e"}' 57 | ); 58 | }); 59 | 60 | test('ReportedElement P toString as expected', () => { 61 | // Given / When 62 | const el: Element = document.createElement('p'); 63 | const ro: src.ReportedElement = new src.ReportedElement( 64 | el, 65 | 'http://localhost/' 66 | ); 67 | 68 | // Then 69 | expect(ro.toNonTimestampString()).toBe( 70 | '{"type":"nodeAdded","tagName":"P","id":"","nodeName":"P","url":"http://localhost/","text":""}' 71 | ); 72 | }); 73 | 74 | test('ReportedElement A toString as expected', () => { 75 | // Given / When 76 | const a: Element = document.createElement('a'); 77 | const linkText = document.createTextNode('Title'); 78 | a.appendChild(linkText); 79 | a.setAttribute('href', 'https://example.com'); 80 | const ro: src.ReportedElement = new src.ReportedElement( 81 | a, 82 | 'http://localhost/' 83 | ); 84 | 85 | // Then 86 | expect(ro.toNonTimestampString()).toBe( 87 | '{"type":"nodeAdded","tagName":"A","id":"","nodeName":"A","url":"http://localhost/","href":"https://example.com/","text":"Title"}' 88 | ); 89 | }); 90 | 91 | test('Report no document links', () => { 92 | // Given 93 | const dom: JSDOM = new JSDOM( 94 | '

No links

' 95 | ); 96 | const mockFn = jest.fn(); 97 | 98 | // When 99 | src.reportPageLinks(dom.window.document, mockFn); 100 | 101 | // Then 102 | expect(mockFn.mock.calls.length).toBe(0); 103 | }); 104 | 105 | test('Report standard page links', () => { 106 | // Given 107 | const dom: JSDOM = new JSDOM( 108 | 'link1link2' 109 | ); 110 | const mockFn = jest.fn(); 111 | 112 | // When 113 | src.reportPageLinks(dom.window.document, mockFn); 114 | 115 | // Then 116 | expect(mockFn.mock.calls.length).toBe(2); 117 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 118 | '{"type":"nodeAdded","tagName":"A","id":"","nodeName":"A","url":"http://localhost/","href":"https://www.example.com/1","text":"link1"}' 119 | ); 120 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 121 | '{"type":"nodeAdded","tagName":"A","id":"","nodeName":"A","url":"http://localhost/","href":"https://www.example.com/2","text":"link2"}' 122 | ); 123 | }); 124 | 125 | test('Report area page links', () => { 126 | // Given 127 | const dom: JSDOM = new JSDOM( 128 | '' 129 | ); 130 | const mockFn = jest.fn(); 131 | 132 | // When 133 | src.reportPageLinks(dom.window.document, mockFn); 134 | 135 | // Then 136 | expect(mockFn.mock.calls.length).toBe(2); 137 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 138 | '{"type":"nodeAdded","tagName":"AREA","id":"","nodeName":"AREA","url":"http://localhost/","href":"https://www.example.com/1","text":""}' 139 | ); 140 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 141 | '{"type":"nodeAdded","tagName":"AREA","id":"","nodeName":"AREA","url":"http://localhost/","href":"https://www.example.com/2","text":""}' 142 | ); 143 | }); 144 | 145 | test('Report no document forms', () => { 146 | // Given 147 | const dom: JSDOM = new JSDOM( 148 | '

No links

' 149 | ); 150 | const mockFn = jest.fn(); 151 | 152 | // When 153 | src.reportPageForms(dom.window.document, mockFn); 154 | 155 | // Then 156 | expect(mockFn.mock.calls.length).toBe(0); 157 | }); 158 | 159 | test('Report page forms', () => { 160 | // Given 161 | const dom: JSDOM = new JSDOM( 162 | '
Content1
Content2
' 163 | ); 164 | const mockFn = jest.fn(); 165 | 166 | // When 167 | src.reportPageForms(dom.window.document, mockFn); 168 | 169 | // Then 170 | expect(mockFn.mock.calls.length).toBe(2); 171 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 172 | '{"type":"nodeAdded","tagName":"FORM","id":"form1","nodeName":"FORM","url":"http://localhost/","text":"Content1","formId":-1}' 173 | ); 174 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 175 | '{"type":"nodeAdded","tagName":"FORM","id":"form2","nodeName":"FORM","url":"http://localhost/","text":"Content2","formId":-1}' 176 | ); 177 | }); 178 | 179 | test('Report node elements', () => { 180 | // Given 181 | const form: Element = document.createElement('form'); 182 | const i1: Element = document.createElement('input'); 183 | i1.setAttribute('id', 'input1'); 184 | const i2: Element = document.createElement('input'); 185 | i2.setAttribute('id', 'input2'); 186 | // This should not be reported as we're just looking for input elements' 187 | const b1: Element = document.createElement('button'); 188 | b1.setAttribute('id', 'button'); 189 | 190 | form.appendChild(i1); 191 | form.appendChild(b1); 192 | form.appendChild(i2); 193 | 194 | // node.id = ''; 195 | const mockFn = jest.fn(); 196 | 197 | // When 198 | src.reportNodeElements(form, 'input', mockFn); 199 | 200 | // Then 201 | expect(mockFn.mock.calls.length).toBe(2); 202 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 203 | '{"type":"nodeAdded","tagName":"INPUT","id":"input1","nodeName":"INPUT","url":"http://localhost/","text":"","tagType":"text","formId":-1}' 204 | ); 205 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 206 | '{"type":"nodeAdded","tagName":"INPUT","id":"input2","nodeName":"INPUT","url":"http://localhost/","text":"","tagType":"text","formId":-1}' 207 | ); 208 | }); 209 | 210 | test('Report storage', () => { 211 | // Given 212 | // localStorage is mocked by Jest 213 | localStorage.setItem('item1', 'value1'); 214 | localStorage.setItem('item2', 'value2'); 215 | localStorage.setItem('item3', 'value3'); 216 | const mockFn = jest.fn(); 217 | 218 | // When 219 | src.reportStorage('localStorage', localStorage, mockFn); 220 | 221 | // Then 222 | expect(mockFn.mock.calls.length).toBe(3); 223 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 224 | '{"type":"localStorage","tagName":"","id":"item1","nodeName":"","url":"http://localhost/","text":"value1"}' 225 | ); 226 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 227 | '{"type":"localStorage","tagName":"","id":"item2","nodeName":"","url":"http://localhost/","text":"value2"}' 228 | ); 229 | expect(mockFn.mock.calls[2][0].toNonTimestampString()).toBe( 230 | '{"type":"localStorage","tagName":"","id":"item3","nodeName":"","url":"http://localhost/","text":"value3"}' 231 | ); 232 | 233 | // Tidy 234 | localStorage.removeItem('item1'); 235 | localStorage.removeItem('item2'); 236 | localStorage.removeItem('item3'); 237 | }); 238 | 239 | test('Reported page loaded', () => { 240 | // Given 241 | const dom: JSDOM = new JSDOM( 242 | '' + 243 | 'link1' + 244 | '
FormContent
' + 245 | '' + 246 | '' + 247 | '' + 248 | '' 249 | ); 250 | const mockFn = jest.fn(); 251 | localStorage.setItem('lsKey', 'value1'); 252 | sessionStorage.setItem('ssKey', 'value2'); 253 | 254 | // When 255 | src.reportPageLoaded(dom.window.document, mockFn); 256 | 257 | // Then 258 | expect(mockFn.mock.calls.length).toBe(8); 259 | expect(mockFn.mock.calls[0][0].toNonTimestampString()).toBe( 260 | '{"type":"nodeAdded","tagName":"A","id":"","nodeName":"A","url":"http://localhost/","href":"https://www.example.com/1","text":"link1"}' 261 | ); 262 | expect(mockFn.mock.calls[1][0].toNonTimestampString()).toBe( 263 | '{"type":"nodeAdded","tagName":"AREA","id":"","nodeName":"AREA","url":"http://localhost/","href":"https://www.example.com/1","text":""}' 264 | ); 265 | expect(mockFn.mock.calls[2][0].toNonTimestampString()).toBe( 266 | '{"type":"nodeAdded","tagName":"FORM","id":"form1","nodeName":"FORM","url":"http://localhost/","text":"FormContent","formId":-1}' 267 | ); 268 | expect(mockFn.mock.calls[3][0].toNonTimestampString()).toBe( 269 | '{"type":"nodeAdded","tagName":"INPUT","id":"input1","nodeName":"INPUT","url":"http://localhost/","text":"default","tagType":"text"}' 270 | ); 271 | expect(mockFn.mock.calls[4][0].toNonTimestampString()).toBe( 272 | '{"type":"nodeAdded","tagName":"INPUT","id":"submit","nodeName":"INPUT","url":"http://localhost/","text":"Submit","tagType":"submit"}' 273 | ); 274 | expect(mockFn.mock.calls[5][0].toNonTimestampString()).toBe( 275 | '{"type":"nodeAdded","tagName":"BUTTON","id":"button1","nodeName":"BUTTON","url":"http://localhost/","text":"Button"}' 276 | ); 277 | expect(mockFn.mock.calls[6][0].toNonTimestampString()).toBe( 278 | '{"type":"localStorage","tagName":"","id":"lsKey","nodeName":"","url":"http://localhost/","text":"value1"}' 279 | ); 280 | expect(mockFn.mock.calls[7][0].toNonTimestampString()).toBe( 281 | '{"type":"sessionStorage","tagName":"","id":"ssKey","nodeName":"","url":"http://localhost/","text":"value2"}' 282 | ); 283 | 284 | // Tidy 285 | localStorage.removeItem('lsKey'); 286 | sessionStorage.removeItem('ssKey'); 287 | }); 288 | 289 | test('Should Disable The Extension', async () => { 290 | // Given / When 291 | const actualOutcome = await src.injectScript(); 292 | // Then 293 | expect(actualOutcome).toBe(false); 294 | }); 295 | 296 | test('should generate valid script', () => { 297 | const script = new ZestScript('recordedScript'); 298 | const expectedOutcome = `{ 299 | "about": "This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/", 300 | "zestVersion": "0.3", 301 | "title": "recordedScript", 302 | "description": "", 303 | "prefix": "", 304 | "type": "StandAlone", 305 | "parameters": { 306 | "tokenStart": "{{", 307 | "tokenEnd": "}}", 308 | "tokens": {}, 309 | "elementType": "ZestVariables" 310 | }, 311 | "statements": [], 312 | "authentication": [], 313 | "index": 0, 314 | "enabled": true, 315 | "elementType": "ZestScript" 316 | }`; 317 | expect(script.toJSON()).toBe(expectedOutcome); 318 | }); 319 | 320 | test('should generate valid click statement', () => { 321 | const elementLocator = new ElementLocator('id', 'test'); 322 | const zestStatementElementClick = new ZestStatementElementClick( 323 | elementLocator, 324 | 1000 325 | ); 326 | 327 | expect(zestStatementElementClick.toJSON()).toBe( 328 | '{"windowHandle":"windowHandle1","type":"id","element":"test","index":-1,"waitForMsec":1000,"enabled":true,"elementType":"ZestClientElementClick"}' 329 | ); 330 | }); 331 | 332 | test('should generate valid send keys statement', () => { 333 | const elementLocator = new ElementLocator('id', 'test'); 334 | const zestStatementElementSendKeys = new ZestStatementElementSendKeys( 335 | elementLocator, 336 | 'testvalue', 337 | 1000 338 | ); 339 | 340 | expect(zestStatementElementSendKeys.toJSON()).toBe( 341 | '{"value":"testvalue","windowHandle":"windowHandle1","type":"id","element":"test","index":-1,"waitForMsec":1000,"enabled":true,"elementType":"ZestClientElementSendKeys"}' 342 | ); 343 | }); 344 | 345 | test('should add zest statement to zest script', () => { 346 | const script = new ZestScript('recordedScript'); 347 | const elementLocator = new ElementLocator('id', 'test'); 348 | const zestStatementElementClick = new ZestStatementElementClick( 349 | elementLocator, 350 | 1000 351 | ); 352 | script.addStatement(zestStatementElementClick.toJSON()); 353 | const expectedOutcome = `{ 354 | "about": "This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/", 355 | "zestVersion": "0.3", 356 | "title": "recordedScript", 357 | "description": "", 358 | "prefix": "", 359 | "type": "StandAlone", 360 | "parameters": { 361 | "tokenStart": "{{", 362 | "tokenEnd": "}}", 363 | "tokens": {}, 364 | "elementType": "ZestVariables" 365 | }, 366 | "statements": [ 367 | { 368 | "windowHandle": "windowHandle1", 369 | "type": "id", 370 | "element": "test", 371 | "index": 1, 372 | "waitForMsec": 1000, 373 | "enabled": true, 374 | "elementType": "ZestClientElementClick" 375 | } 376 | ], 377 | "authentication": [], 378 | "index": 0, 379 | "enabled": true, 380 | "elementType": "ZestScript" 381 | }`; 382 | expect(script.toJSON()).toBe(expectedOutcome); 383 | }); 384 | 385 | test('should reset zest script', () => { 386 | const script = new ZestScript('recordedScript'); 387 | const elementLocator = new ElementLocator('id', 'test'); 388 | const zestStatementElementClick = new ZestStatementElementClick( 389 | elementLocator, 390 | 1000 391 | ); 392 | script.addStatement(zestStatementElementClick.toJSON()); 393 | script.reset(); 394 | const expectedOutcome = `{ 395 | "about": "This is a Zest script. For more details about Zest visit https://github.com/zaproxy/zest/", 396 | "zestVersion": "0.3", 397 | "title": "recordedScript", 398 | "description": "", 399 | "prefix": "", 400 | "type": "StandAlone", 401 | "parameters": { 402 | "tokenStart": "{{", 403 | "tokenEnd": "}}", 404 | "tokens": {}, 405 | "elementType": "ZestVariables" 406 | }, 407 | "statements": [], 408 | "authentication": [], 409 | "index": 0, 410 | "enabled": true, 411 | "elementType": "ZestScript" 412 | }`; 413 | expect(script.toJSON()).toBe(expectedOutcome); 414 | }); 415 | 416 | test('should return correct path for element with id', () => { 417 | // Given 418 | const dom: JSDOM = new JSDOM( 419 | '
' 420 | ); 421 | const element = dom.window.document.getElementById('myElement'); 422 | 423 | // When 424 | const path = getPath(element as HTMLElement, dom.window.document); 425 | 426 | // Then 427 | expect(path.type).toBe('id'); 428 | expect(path.element).toBe('myElement'); 429 | }); 430 | 431 | test('should return correct path for element with unique class', () => { 432 | // Given 433 | const dom: JSDOM = new JSDOM( 434 | '
' 435 | ); 436 | const element = dom.window.document.querySelector('.myClass'); 437 | 438 | // When 439 | const path = getPath(element as HTMLElement, dom.window.document); 440 | 441 | // Then 442 | expect(path.type).toBe('className'); 443 | expect(path.element).toBe('myClass'); 444 | }); 445 | 446 | test('should return correct path for element with CSS selector', () => { 447 | // Given 448 | const dom: JSDOM = new JSDOM( 449 | '
' 450 | ); 451 | const element = dom.window.document.getElementsByTagName('button')[0]; 452 | 453 | // When 454 | const path = getPath(element as HTMLElement, dom.window.document); 455 | 456 | // Then 457 | expect(path.type).toBe('cssSelector'); 458 | expect(path.element).toBe('body > div > span > button'); 459 | }); 460 | 461 | test('should return correct path for element with XPath', () => { 462 | // Given 463 | const dom = new JSDOM( 464 | '
' 465 | ); 466 | const element = dom.window.document.getElementsByClassName('btn'); 467 | 468 | // When 469 | const path1 = getPath(element[0] as HTMLElement, dom.window.document); 470 | const path2 = getPath(element[1] as HTMLElement, dom.window.document); 471 | 472 | // Then 473 | expect(path1.type).toBe('xpath'); 474 | expect(path1.element).toBe('/html/body/div/button[1]'); 475 | expect(path2.type).toBe('xpath'); 476 | expect(path2.element).toBe('/html/body/div/button[2]'); 477 | }); 478 | 479 | test('should generate valid frame switch statement', () => { 480 | const zestStatementSwitchToFrame = new ZestStatementSwitchToFrame( 481 | 0, 482 | 'testvalue' 483 | ); 484 | 485 | expect(zestStatementSwitchToFrame.toJSON()).toBe( 486 | '{"windowHandle":"windowHandle1","frameIndex":0,"frameName":"testvalue","parent":false,"index":-1,"enabled":true,"elementType":"ZestClientSwitchToFrame"}' 487 | ); 488 | }); 489 | -------------------------------------------------------------------------------- /test/ContentScript/utils.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import http from 'http'; 21 | import fs from 'fs'; 22 | import path from 'path'; 23 | import {Request, Response} from 'express'; 24 | import JsonServer from 'json-server'; 25 | 26 | export function getStaticHttpServer(): http.Server { 27 | return http.createServer((request, response) => { 28 | const url = `${request.url}`; 29 | 30 | if (url.startsWith('/redirect/')) { 31 | response.writeHead(302, {Location: '/webpages/interactions.html'}); 32 | response.end(); 33 | return; 34 | } 35 | 36 | const filePath = path.join(__dirname, url); 37 | 38 | fs.promises 39 | .access(filePath, fs.constants.F_OK) 40 | .then(() => { 41 | const fileStream = fs.createReadStream(filePath); 42 | response.writeHead(200, {'Content-Type': 'text/html'}); 43 | fileStream.pipe(response); 44 | }) 45 | .catch((err: NodeJS.ErrnoException | null) => { 46 | response.writeHead(404, {'Content-Type': 'text/plain'}); 47 | response.end(`Error : ${err}`); 48 | }); 49 | }); 50 | } 51 | 52 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 53 | function getInsertPosition(body: any, actualData: Array): number { 54 | const statementJson = body?.statementJson; 55 | if (statementJson) { 56 | const index = JSON.parse(statementJson)?.index; 57 | if (index) { 58 | return index - 1; 59 | } 60 | } 61 | return actualData.length; 62 | } 63 | 64 | function toJsonWithoutDynamicValues(value: string): string { 65 | return JSON.parse( 66 | value 67 | .replace(/timestamp":\d+/g, 'timestamp": "TIMESTAMP"') 68 | .replace(/Recorded by [^\\]+?"/g, 'Recorded by comment"') 69 | .replace(/browserType":"[^\\]+?"/g, 'browserType":"browser"') 70 | ); 71 | } 72 | 73 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 74 | function normalizeJson(body: any): any { 75 | ['eventJson', 'statementJson', 'objectJson'].forEach((name) => { 76 | const value = body[name]; 77 | if (value) { 78 | body[name] = toJsonWithoutDynamicValues(value); 79 | } 80 | }); 81 | return body; 82 | } 83 | 84 | export function getFakeZapServer( 85 | actualData: Array, 86 | JSONPORT: number, 87 | incZapEvents = false 88 | ): http.Server { 89 | const app = JsonServer.create(); 90 | 91 | app.use(JsonServer.bodyParser); 92 | app.post('/JSON/client/action/:action', (req: Request, res: Response) => { 93 | const action = req.params; 94 | const {body} = req; 95 | const msg = JSON.stringify({action, body}); 96 | if (incZapEvents || msg.indexOf('localzap') === -1) { 97 | // Ignore localzap events 98 | actualData[getInsertPosition(body, actualData)] = { 99 | action, 100 | body: normalizeJson(body), 101 | }; 102 | } 103 | res.sendStatus(200); 104 | }); 105 | 106 | return app.listen(JSONPORT, () => { 107 | console.log(`JSON Server listening on port ${JSONPORT}`); 108 | }); 109 | } 110 | 111 | export function reportEvent(eventName: string, url: string): object { 112 | const data = { 113 | action: {action: 'reportEvent'}, 114 | body: { 115 | eventJson: { 116 | timestamp: 'TIMESTAMP', 117 | eventName, 118 | url, 119 | count: 1, 120 | }, 121 | apikey: 'not set', 122 | }, 123 | }; 124 | return data; 125 | } 126 | 127 | export function reportObject( 128 | type: string, 129 | tagName: string, 130 | id: string, 131 | nodeName: string, 132 | url: string, 133 | href: string | undefined, 134 | text: string 135 | ): object { 136 | const data = { 137 | action: {action: 'reportObject'}, 138 | body: { 139 | objectJson: { 140 | timestamp: 'TIMESTAMP', 141 | type, 142 | tagName, 143 | id, 144 | nodeName, 145 | url, 146 | href, 147 | text, 148 | }, 149 | apikey: 'not set', 150 | }, 151 | }; 152 | if (href === undefined) { 153 | delete data.body.objectJson.href; 154 | } 155 | return data; 156 | } 157 | 158 | export function reportZestStatementComment(): object { 159 | const data = { 160 | action: { 161 | action: 'reportZestStatement', 162 | }, 163 | body: { 164 | statementJson: { 165 | index: 1, 166 | enabled: true, 167 | elementType: 'ZestComment', 168 | comment: 'Recorded by comment', 169 | }, 170 | apikey: 'not set', 171 | }, 172 | }; 173 | return data; 174 | } 175 | 176 | export function reportZestStatementLaunch(url: string): object { 177 | const data = { 178 | action: {action: 'reportZestStatement'}, 179 | body: { 180 | statementJson: { 181 | windowHandle: 'windowHandle1', 182 | browserType: 'browser', 183 | url, 184 | capabilities: '', 185 | headless: false, 186 | index: 2, 187 | enabled: true, 188 | elementType: 'ZestClientLaunch', 189 | }, 190 | apikey: 'not set', 191 | }, 192 | }; 193 | return data; 194 | } 195 | 196 | export function reportZestStatementClose(index: number): object { 197 | const data = { 198 | action: {action: 'reportZestStatement'}, 199 | body: { 200 | statementJson: { 201 | windowHandle: 'windowHandle1', 202 | index, 203 | sleepInSeconds: 0, 204 | enabled: true, 205 | elementType: 'ZestClientWindowClose', 206 | }, 207 | apikey: 'not set', 208 | }, 209 | }; 210 | return data; 211 | } 212 | 213 | function reportZestStatement( 214 | index: number, 215 | elementType: string, 216 | element: string, 217 | value: string | undefined = undefined 218 | ): object { 219 | const data = { 220 | action: {action: 'reportZestStatement'}, 221 | body: { 222 | statementJson: { 223 | windowHandle: 'windowHandle1', 224 | type: 'id', 225 | element, 226 | index, 227 | waitForMsec: 5000, 228 | enabled: true, 229 | elementType, 230 | value, 231 | }, 232 | apikey: 'not set', 233 | }, 234 | }; 235 | if (value === undefined) { 236 | delete data.body.statementJson.value; 237 | } 238 | return data; 239 | } 240 | 241 | export function reportZestStatementScrollTo( 242 | index: number, 243 | element: string 244 | ): object { 245 | return reportZestStatement( 246 | index, 247 | 'ZestClientElementScrollTo', 248 | element, 249 | undefined 250 | ); 251 | } 252 | 253 | export function reportZestStatementClick( 254 | index: number, 255 | element: string 256 | ): object { 257 | return reportZestStatement( 258 | index, 259 | 'ZestClientElementClick', 260 | element, 261 | undefined 262 | ); 263 | } 264 | 265 | export function reportZestStatementSubmit( 266 | index: number, 267 | element: string 268 | ): object { 269 | return reportZestStatement( 270 | index, 271 | 'ZestClientElementSubmit', 272 | element, 273 | undefined 274 | ); 275 | } 276 | 277 | export function reportZestStatementSendKeys( 278 | index: number, 279 | element: string, 280 | value: string 281 | ): object { 282 | return reportZestStatement( 283 | index, 284 | 'ZestClientElementSendKeys', 285 | element, 286 | value 287 | ); 288 | } 289 | 290 | export function reportZestStatementSwitchToFrame( 291 | index: number, 292 | frameIndex: number, 293 | frameName: string 294 | ): object { 295 | const data = { 296 | action: { 297 | action: 'reportZestStatement', 298 | }, 299 | body: { 300 | statementJson: { 301 | windowHandle: 'windowHandle1', 302 | frameIndex, 303 | frameName, 304 | parent: false, 305 | index, 306 | enabled: true, 307 | elementType: 'ZestClientSwitchToFrame', 308 | }, 309 | apikey: 'not set', 310 | }, 311 | }; 312 | return data; 313 | } 314 | 315 | export async function closeServer(_server: http.Server): Promise { 316 | return new Promise((resolve) => { 317 | _server.close(() => { 318 | console.log('Server closed'); 319 | resolve(); 320 | }); 321 | }); 322 | } 323 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/integrationTest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | Link 8 | 12 | 13 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/interactions.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/linkedpage1.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Links Page 2 5 | 6 | 7 | Page 2 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/linkedpage2.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Links Page 3 5 | 6 | 7 | Page 3 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/linkedpage3.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Links Page 1 5 | 6 | 7 | Page 1 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/localStorage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/localStorageDelay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/sessionStorage.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 10 | 11 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/sessionStorageDelay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /test/ContentScript/webpages/testFrame.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Document 5 | 6 | 7 | 8 | 14 | 15 | -------------------------------------------------------------------------------- /test/drivers/ChromeDriver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import {BrowserContext, chromium, Page} from 'playwright'; 21 | import {extensionPath} from '../ContentScript/constants'; 22 | 23 | class ChromeDriver { 24 | context: BrowserContext; 25 | 26 | public async getExtensionId(): Promise { 27 | let [background] = this.context.serviceWorkers(); 28 | if (!background) 29 | background = await this.context.waitForEvent('serviceworker'); 30 | return background.url().split('/')[2]; 31 | } 32 | 33 | public async configureExtension(JSONPORT: number): Promise { 34 | const extensionId = await this.getExtensionId(); 35 | const backgroundPage = await this.context.newPage(); 36 | await backgroundPage.goto(`chrome-extension://${extensionId}/options.html`); 37 | await backgroundPage.fill('#zapurl', `http://localhost:${JSONPORT}/`); 38 | await backgroundPage.check('#zapenable'); 39 | await backgroundPage.click('#save'); 40 | await backgroundPage.close(); 41 | } 42 | 43 | public async getContext( 44 | JSONPORT: number, 45 | startRecording = false 46 | ): Promise { 47 | if (this.context) return this.context; 48 | this.context = await chromium.launchPersistentContext('', { 49 | channel: 'chromium', 50 | args: [ 51 | `--disable-extensions-except=${extensionPath.CHROME}-ext`, 52 | `--load-extension=${extensionPath.CHROME}-ext`, 53 | ], 54 | }); 55 | await this.configureExtension(JSONPORT); 56 | if (startRecording) { 57 | await this.toggleRecording(); 58 | } 59 | return this.context; 60 | } 61 | 62 | public async setEnable(value: boolean): Promise { 63 | const page = await this.context.newPage(); 64 | await page.goto(await this.getOptionsURL()); 65 | if (value) { 66 | await page.check('#zapenable'); 67 | } else { 68 | await page.uncheck('#zapenable'); 69 | } 70 | await page.click('#save'); 71 | await page.close(); 72 | } 73 | 74 | public async toggleRecording(loginUrl = ''): Promise { 75 | const page = await this.context.newPage(); 76 | await page.goto(await this.getPopupURL()); 77 | if (loginUrl !== '') { 78 | await page.fill('#login-url-input', loginUrl); 79 | } 80 | await page.click('#record-btn'); 81 | await page.close(); 82 | return loginUrl !== '' ? (this.context.pages().at(-1) as Page) : undefined; 83 | } 84 | 85 | public async close(): Promise { 86 | await this.context?.close(); 87 | } 88 | 89 | public async getOptionsURL(): Promise { 90 | const extensionId = await this.getExtensionId(); 91 | return `chrome-extension://${extensionId}/options.html`; 92 | } 93 | 94 | public async getPopupURL(): Promise { 95 | const extensionId = await this.getExtensionId(); 96 | return `chrome-extension://${extensionId}/popup.html`; 97 | } 98 | } 99 | 100 | export {ChromeDriver}; 101 | -------------------------------------------------------------------------------- /test/drivers/FirefoxDriver.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Zed Attack Proxy (ZAP) and its related source files. 3 | * 4 | * ZAP is an HTTP/HTTPS proxy for assessing web application security. 5 | * 6 | * Copyright 2023 The ZAP Development Team 7 | * 8 | * Licensed under the Apache License, Version 2.0 (the "License"); 9 | * you may not use this file except in compliance with the License. 10 | * You may obtain a copy of the License at 11 | * 12 | * http://www.apache.org/licenses/LICENSE-2.0 13 | * 14 | * Unless required by applicable law or agreed to in writing, software 15 | * distributed under the License is distributed on an "AS IS" BASIS, 16 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | * See the License for the specific language governing permissions and 18 | * limitations under the License. 19 | */ 20 | import {Browser, BrowserContext, firefox} from 'playwright'; 21 | import {withExtension} from 'playwright-webextext'; 22 | import {extensionPath} from '../ContentScript/constants'; 23 | 24 | class FirefoxDriver { 25 | browser: Browser; 26 | 27 | context: BrowserContext; 28 | 29 | extensionId: string; 30 | 31 | public getExtensionId(): string { 32 | // TODO: Find a way to get the extension ID 33 | return this.extensionId; 34 | } 35 | 36 | public async getBrowser(): Promise { 37 | return withExtension(firefox, `${extensionPath.FIREFOX}-ext`).launch({ 38 | headless: false, 39 | }); 40 | } 41 | 42 | public async grantPermission(): Promise { 43 | const page = await this.context.newPage(); 44 | await page.goto('about:addons'); 45 | await page.waitForTimeout(50); 46 | await page.keyboard.press('Tab'); 47 | await page.waitForTimeout(50); 48 | await page.keyboard.press('ArrowDown'); 49 | await page.waitForTimeout(50); 50 | 51 | for (let i = 0; i < 7; i += 1) { 52 | await page.keyboard.press('Tab'); 53 | } 54 | 55 | await page.keyboard.press('Enter'); 56 | 57 | for (let i = 0; i < 4; i += 1) { 58 | await page.keyboard.press('ArrowDown'); 59 | await page.waitForTimeout(50); 60 | } 61 | 62 | await page.keyboard.press('Enter'); 63 | await page.waitForTimeout(50); 64 | await page.keyboard.press('Tab'); 65 | await page.waitForTimeout(50); 66 | await page.keyboard.press('ArrowRight'); 67 | await page.waitForTimeout(50); 68 | await page.keyboard.press('Enter'); 69 | await page.waitForTimeout(50); 70 | await page.keyboard.press('Tab'); 71 | await page.waitForTimeout(50); 72 | await page.keyboard.press('Enter'); 73 | await page.waitForTimeout(50); 74 | 75 | await page.keyboard.down('ShiftLeft'); 76 | await page.waitForTimeout(50); 77 | await page.keyboard.press('Tab'); 78 | await page.waitForTimeout(50); 79 | await page.keyboard.press('Tab'); 80 | await page.waitForTimeout(50); 81 | await page.keyboard.up('ShiftLeft'); 82 | await page.waitForTimeout(50); 83 | 84 | await page.keyboard.press('Enter'); 85 | for (let i = 0; i < 2; i += 1) { 86 | await page.keyboard.press('ArrowDown'); 87 | await page.waitForTimeout(50); 88 | } 89 | await page.keyboard.press('Enter'); 90 | await page.waitForTimeout(50); 91 | await page.close(); 92 | } 93 | 94 | public async getContext(): Promise { 95 | if (this.context) return this.context; 96 | this.browser = await this.getBrowser(); 97 | this.context = await this.browser.newContext(); 98 | // TODO: add way to configure extension 99 | await this.grantPermission(); 100 | return this.context; 101 | } 102 | 103 | public async close(): Promise { 104 | await this.context?.close(); 105 | await this.browser?.close(); 106 | } 107 | 108 | public async getOptionsURL(): Promise { 109 | const extensionId = this.getExtensionId(); 110 | return `moz-extension://${extensionId}/options.html`; 111 | } 112 | 113 | public async getPopupURL(): Promise { 114 | const extensionId = this.getExtensionId(); 115 | return `moz-extension://${extensionId}/popup.html`; 116 | } 117 | 118 | public async setEnable(): Promise { 119 | // TODO: to be implemented 120 | } 121 | 122 | public async toggleRecording(): Promise { 123 | // TODO: to be implemented 124 | } 125 | } 126 | 127 | export {FirefoxDriver}; 128 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@abhijithvijayan/tsconfig", 3 | "compilerOptions": { 4 | "target": "es5", 5 | "module": "esnext", 6 | "lib": [ 7 | "dom", 8 | "dom.iterable", 9 | "esnext" 10 | ], 11 | "declaration": false, 12 | "isolatedModules": true, 13 | "useDefineForClassFields": true, 14 | "skipLibCheck": true 15 | }, 16 | "include": [ 17 | "source", 18 | "webpack.config.js" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /views/basic.scss: -------------------------------------------------------------------------------- 1 | @import "../styles/fonts"; 2 | @import "../styles/variables"; 3 | @import "~webext-base-css/webext-base.css"; 4 | 5 | @font-face { 6 | font-family: 'Roboto'; 7 | font-style: normal; 8 | font-weight: 400; 9 | src: url('../assets/fonts/Roboto-Regular.ttf') format('truetype'); 10 | } 11 | 12 | @font-face { 13 | font-family: 'Roboto'; 14 | font-style: normal; 15 | font-weight: 700; 16 | src: url('../assets/fonts/Roboto-Bold.ttf') format('truetype'); 17 | } 18 | 19 | body { 20 | font-family: "Roboto", sans-serif; 21 | margin: 0px; 22 | } 23 | -------------------------------------------------------------------------------- /views/help.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

ZAP by Checkmarx Recorder

11 | 12 |

Summary

13 | This extension allows you to record all of the actions you take in the browser as a Zest script. 14 | It can be used to record things like authentication scripts or other complex interactions. 15 | Zest scripts can be replayed in ZAP, whether in the desktop or in automation. 16 |

17 | When the extension is recording then a notification panel will be shown at the bottom of the page. 18 | Clicking on the "Stop and Download Recording" button will stop the recording and automatically download the 19 | recording using a filename based on the hostname of the current page and the current date / time. 20 |

21 | You can also stop the recording via the extension dialog and then download the recording using a filename you specify. 22 |

23 | If you are using the full ZAP extension and start recording in ZAP then the Zest script will be automatically updated in 24 | the ZAP Script Console so you do not need to download it manually. 25 | 26 |

Recorder Advice and Guidance

27 | If you are going to use the recorded script for authentication then you need to make sure that the browser 28 | will be in the same state as when it is launched from ZAP. 29 |

30 | If the login URL is static then you can open that page before starting to record.
31 | If the URL is dynamic then you should enter a suitable static URL in the Recorder Dialog. This URL will then 32 | be recorded in the script and the browser will handle the dynamic redirects as required. 33 |

34 | In all cases you should start to record before dismissing any dialogs, such as cookie warnings and other disclaimers, 35 | as ZAP will need to do the same things. 36 |

37 | It is often better to use private/incognito mode when recording so that the browser will not have any existing 38 | application state. 39 |

40 | The 'buttons' on some modern web apps can be complicated HTML components that are sometimes hard to click on using automation. 41 | If your forms can be submitted using the RETURN key then that is often a better option to use when recording. 42 | 43 |

Recorder Dialog

44 | Clicking on the ZAP Extension icon will display the Recorder Dialog.
45 | This dialog has the following components: 46 | 47 |

Start / Stop Recording Button

48 | Click to start and stop the recording. 49 | 50 |

Login URL Field

51 | By default ZAP will start recording based on the URL in the current tab. 52 | If that is a suitable URL then you do not need to use this field.
53 | However in some cases you might not be able to choose a suitable URL.
54 | For example if the initial URL you need to choose automatically redirects to a one-time URL then by default 55 | the recorder would start from the one-time URL. In cases like this enter the URL the recorder should start from 56 | in this Login URL field. When you click on the Start Recording button then the extension will start recording using 57 | the URL you have given and open it in a new tab. 58 | 59 |

Script Name Field

60 | The name of the script that the recorder will download from this dialog. 61 |

62 | If you stop recording via the "Stop and Download Recording" button in the notification panel then the extension will 63 | automatically download the recording using an auto-generated filename. 64 | 65 |

Download Script Button

66 | Clicking on this button will download any recorded script using the name in the above field. 67 | You must have recorded something and specified a script name. 68 | 69 |

Config Button

70 | Shows the extension configuration screen. This is only available in the full ZAP extension, the recorder extension 71 | does not have this button or any configuration options. 72 | 73 |

Help Button

74 | Shows this screen. 75 | 76 | 77 | -------------------------------------------------------------------------------- /views/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |

ZAP by Checkmarx Browser Extension

11 | This extension gives ZAP access to the browser. 12 |
13 | These options should be set correctly if the browser is launched from ZAP but may need to be set manually if you 14 | install this add-on in a browser not launched by ZAP. 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
For example http://zap/ or http://localhost:8080/
As set in the ZAP API options
40 |
41 | 42 | For more info see https://github.com/zaproxy/browser-extension/ 43 | 44 | -------------------------------------------------------------------------------- /views/popup.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | ZAP Browser Extension 8 | 9 | 10 | 11 |
12 |
13 |

14 | 15 |

16 | 17 |
18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 |
27 | 28 | 29 |
30 | 31 |
32 |
33 | 34 |
35 |
36 | 37 |
38 |
39 | 40 |
41 |
42 | 43 |
44 |
45 |
46 |
47 | 48 | 49 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const FilemanagerPlugin = require('filemanager-webpack-plugin'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 8 | const ExtensionReloader = require('webpack-ext-reloader'); 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin'); 11 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 12 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 13 | 14 | const viewsPath = path.join(__dirname, 'views'); 15 | const sourcePath = path.join(__dirname, 'source'); 16 | const destPath = path.join(__dirname, 'extension'); 17 | const nodeEnv = process.env.NODE_ENV || 'development'; 18 | const targetBrowser = process.env.TARGET_BROWSER; 19 | 20 | const extensionReloaderPlugin = 21 | nodeEnv === 'development' 22 | ? new ExtensionReloader({ 23 | port: 9090, 24 | reloadPage: true, 25 | entries: { 26 | // TODO: reload manifest on update 27 | contentScript: 'contentScript', 28 | background: 'background', 29 | extensionPage: ['options'], 30 | }, 31 | }) 32 | : () => { 33 | this.apply = () => {}; 34 | }; 35 | 36 | const getExtensionFileType = (browser) => { 37 | if (browser === 'opera') { 38 | return 'crx'; 39 | } 40 | 41 | if (browser === 'firefox') { 42 | return 'xpi'; 43 | } 44 | 45 | return 'zip'; 46 | }; 47 | 48 | module.exports = { 49 | devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342 50 | 51 | stats: { 52 | all: false, 53 | builtAt: true, 54 | errors: true, 55 | hash: true, 56 | }, 57 | 58 | mode: nodeEnv, 59 | 60 | entry: { 61 | manifest: path.join(sourcePath, 'manifest.json'), 62 | background: path.join(sourcePath, 'Background', 'index.ts'), 63 | contentScript: path.join(sourcePath, 'ContentScript', 'index.ts'), 64 | options: path.join(sourcePath, 'Options', 'index.tsx'), 65 | popup: path.join(sourcePath, 'Popup', 'index.tsx'), 66 | }, 67 | 68 | output: { 69 | path: path.join(destPath, `${targetBrowser}-ext`), 70 | filename: 'js/[name].bundle.js', 71 | }, 72 | 73 | resolve: { 74 | extensions: ['.ts', '.tsx', '.js', '.json'], 75 | alias: { 76 | 'webextension-polyfill-ts': path.resolve( 77 | path.join(__dirname, 'node_modules', 'webextension-polyfill-ts') 78 | ), 79 | }, 80 | }, 81 | 82 | module: { 83 | rules: [ 84 | { 85 | type: 'javascript/auto', // prevent webpack handling json with its own loaders, 86 | test: /manifest\.json$/, 87 | use: { 88 | loader: 'wext-manifest-loader', 89 | options: { 90 | usePackageJSONVersion: false, // set to false to not use package.json version for manifest 91 | }, 92 | }, 93 | exclude: /node_modules/, 94 | }, 95 | { 96 | test: /\.(js|ts)x?$/, 97 | loader: 'babel-loader', 98 | exclude: /node_modules/, 99 | }, 100 | { 101 | test: /\.(sa|sc|c)ss$/, 102 | use: [ 103 | { 104 | loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS 105 | }, 106 | { 107 | loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack 108 | options: { 109 | sourceMap: true, 110 | }, 111 | }, 112 | { 113 | loader: 'postcss-loader', 114 | options: { 115 | postcssOptions: { 116 | plugins: [ 117 | [ 118 | 'autoprefixer', 119 | { 120 | // Options 121 | }, 122 | ], 123 | ], 124 | }, 125 | }, 126 | }, 127 | 'resolve-url-loader', // Rewrites relative paths in url() statements 128 | 'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS 129 | ], 130 | }, 131 | ], 132 | }, 133 | 134 | plugins: [ 135 | // Plugin to not generate js bundle for manifest entry 136 | new WextManifestWebpackPlugin(), 137 | // Generate sourcemaps 138 | new webpack.SourceMapDevToolPlugin({filename: false}), 139 | new ForkTsCheckerWebpackPlugin(), 140 | // environmental variables 141 | new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']), 142 | // delete previous build files 143 | new CleanWebpackPlugin({ 144 | cleanOnceBeforeBuildPatterns: [ 145 | path.join(process.cwd(), `extension/${targetBrowser}-ext`), 146 | path.join( 147 | process.cwd(), 148 | `extension/${targetBrowser}.ext.${getExtensionFileType(targetBrowser)}` 149 | ), 150 | ], 151 | cleanStaleWebpackAssets: false, 152 | verbose: true, 153 | }), 154 | new HtmlWebpackPlugin({ 155 | template: path.join(viewsPath, 'options.html'), 156 | inject: 'body', 157 | chunks: ['options'], 158 | hash: true, 159 | filename: 'options.html', 160 | }), 161 | new HtmlWebpackPlugin({ 162 | template: path.join(viewsPath, 'help.html'), 163 | inject: 'body', 164 | chunks: ['help'], 165 | hash: true, 166 | filename: 'help.html', 167 | }), 168 | new HtmlWebpackPlugin({ 169 | template: path.join(viewsPath, 'popup.html'), 170 | inject: 'body', 171 | chunks: ['popup'], 172 | hash: true, 173 | filename: 'popup.html', 174 | }), 175 | // write css file(s) to build folder 176 | new MiniCssExtractPlugin({filename: 'css/[name].css'}), 177 | new MiniCssExtractPlugin({filename: 'basic.css'}), 178 | // copy static assets 179 | new CopyWebpackPlugin({ 180 | patterns: [{from: 'source/assets', to: 'assets'}], 181 | }), 182 | // plugin to enable browser reloading in development mode 183 | extensionReloaderPlugin, 184 | ], 185 | 186 | optimization: { 187 | minimize: true, 188 | minimizer: [ 189 | new TerserPlugin({ 190 | parallel: true, 191 | terserOptions: { 192 | format: { 193 | comments: false, 194 | }, 195 | }, 196 | extractComments: false, 197 | }), 198 | new OptimizeCSSAssetsPlugin({ 199 | cssProcessorPluginOptions: { 200 | preset: ['default', {discardComments: {removeAll: true}}], 201 | }, 202 | }), 203 | new FilemanagerPlugin({ 204 | events: { 205 | onEnd: { 206 | archive: [ 207 | { 208 | format: 'zip', 209 | source: path.join(destPath, `${targetBrowser}-ext`), 210 | destination: `${path.join(destPath, targetBrowser)}.ext.${getExtensionFileType(targetBrowser)}`, 211 | options: {zlib: {level: 6}}, 212 | }, 213 | ], 214 | }, 215 | }, 216 | }), 217 | ], 218 | }, 219 | }; -------------------------------------------------------------------------------- /webpack.config.rec.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const webpack = require('webpack'); 3 | const FilemanagerPlugin = require('filemanager-webpack-plugin'); 4 | const TerserPlugin = require('terser-webpack-plugin'); 5 | const CopyWebpackPlugin = require('copy-webpack-plugin'); 6 | const HtmlWebpackPlugin = require('html-webpack-plugin'); 7 | const {CleanWebpackPlugin} = require('clean-webpack-plugin'); 8 | const ExtensionReloader = require('webpack-ext-reloader'); 9 | const MiniCssExtractPlugin = require('mini-css-extract-plugin'); 10 | const WextManifestWebpackPlugin = require('wext-manifest-webpack-plugin'); 11 | const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin'); 12 | const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin'); 13 | 14 | const viewsPath = path.join(__dirname, 'views'); 15 | const sourcePath = path.join(__dirname, 'source'); 16 | const destPath = path.join(__dirname, 'extension'); 17 | const nodeEnv = process.env.NODE_ENV || 'development'; 18 | const targetBrowser = process.env.TARGET_BROWSER; 19 | 20 | const extensionReloaderPlugin = 21 | nodeEnv === 'development' 22 | ? new ExtensionReloader({ 23 | port: 9090, 24 | reloadPage: true, 25 | entries: { 26 | // TODO: reload manifest on update 27 | contentScript: 'contentScript', 28 | background: 'background', 29 | }, 30 | }) 31 | : () => { 32 | this.apply = () => {}; 33 | }; 34 | 35 | const getExtensionFileType = (browser) => { 36 | if (browser === 'opera') { 37 | return 'crx'; 38 | } 39 | 40 | if (browser === 'firefox') { 41 | return 'xpi'; 42 | } 43 | 44 | return 'zip'; 45 | }; 46 | 47 | module.exports = { 48 | devtool: false, // https://github.com/webpack/webpack/issues/1194#issuecomment-560382342 49 | 50 | stats: { 51 | all: false, 52 | builtAt: true, 53 | errors: true, 54 | hash: true, 55 | }, 56 | 57 | mode: nodeEnv, 58 | 59 | entry: { 60 | manifest: path.join(sourcePath, 'manifest.rec.json'), 61 | background: path.join(sourcePath, 'Background', 'index.ts'), 62 | contentScript: path.join(sourcePath, 'ContentScript', 'index.ts'), 63 | popup: path.join(sourcePath, 'Popup', 'index.tsx'), 64 | }, 65 | 66 | output: { 67 | path: path.join(destPath, `${targetBrowser}-rec`), 68 | filename: 'js/[name].bundle.js', 69 | }, 70 | 71 | resolve: { 72 | extensions: ['.ts', '.tsx', '.js', '.json'], 73 | alias: { 74 | 'webextension-polyfill-ts': path.resolve( 75 | path.join(__dirname, 'node_modules', 'webextension-polyfill-ts') 76 | ), 77 | }, 78 | }, 79 | 80 | module: { 81 | rules: [ 82 | { 83 | type: 'javascript/auto', // prevent webpack handling json with its own loaders, 84 | test: /manifest\.rec\.json$/, 85 | use: { 86 | loader: 'wext-manifest-loader', 87 | options: { 88 | usePackageJSONVersion: false, // set to false to not use package.json version for manifest 89 | }, 90 | }, 91 | exclude: /node_modules/, 92 | }, 93 | { 94 | test: /\.(js|ts)x?$/, 95 | loader: 'babel-loader', 96 | exclude: /node_modules/, 97 | }, 98 | { 99 | test: /\.(sa|sc|c)ss$/, 100 | use: [ 101 | { 102 | loader: MiniCssExtractPlugin.loader, // It creates a CSS file per JS file which contains CSS 103 | }, 104 | { 105 | loader: 'css-loader', // Takes the CSS files and returns the CSS with imports and url(...) for Webpack 106 | options: { 107 | sourceMap: true, 108 | }, 109 | }, 110 | { 111 | loader: 'postcss-loader', 112 | options: { 113 | postcssOptions: { 114 | plugins: [ 115 | [ 116 | 'autoprefixer', 117 | { 118 | // Options 119 | }, 120 | ], 121 | ], 122 | }, 123 | }, 124 | }, 125 | 'resolve-url-loader', // Rewrites relative paths in url() statements 126 | 'sass-loader', // Takes the Sass/SCSS file and compiles to the CSS 127 | ], 128 | }, 129 | ], 130 | }, 131 | 132 | plugins: [ 133 | // Plugin to not generate js bundle for manifest entry 134 | new WextManifestWebpackPlugin(), 135 | // Generate sourcemaps 136 | new webpack.SourceMapDevToolPlugin({filename: false}), 137 | new ForkTsCheckerWebpackPlugin(), 138 | // environmental variables 139 | new webpack.EnvironmentPlugin(['NODE_ENV', 'TARGET_BROWSER']), 140 | // delete previous build files 141 | new CleanWebpackPlugin({ 142 | cleanOnceBeforeBuildPatterns: [ 143 | path.join(process.cwd(), `extension/${targetBrowser}-rec`), 144 | path.join( 145 | process.cwd(), 146 | `extension/${targetBrowser}.rec.${getExtensionFileType(targetBrowser)}` 147 | ), 148 | ], 149 | cleanStaleWebpackAssets: false, 150 | verbose: true, 151 | }), 152 | new HtmlWebpackPlugin({ 153 | template: path.join(viewsPath, 'help.html'), 154 | inject: 'body', 155 | chunks: ['help'], 156 | hash: true, 157 | filename: 'help.html', 158 | }), 159 | new HtmlWebpackPlugin({ 160 | template: path.join(viewsPath, 'popup.html'), 161 | inject: 'body', 162 | chunks: ['popup'], 163 | hash: true, 164 | filename: 'popup.html', 165 | }), 166 | // write css file(s) to build folder 167 | new MiniCssExtractPlugin({filename: 'css/[name].css'}), 168 | new MiniCssExtractPlugin({filename: 'basic.css'}), 169 | // copy static assets 170 | new CopyWebpackPlugin({ 171 | patterns: [{from: 'source/assets', to: 'assets'}], 172 | }), 173 | // plugin to enable browser reloading in development mode 174 | extensionReloaderPlugin, 175 | ], 176 | 177 | optimization: { 178 | minimize: true, 179 | minimizer: [ 180 | new TerserPlugin({ 181 | parallel: true, 182 | terserOptions: { 183 | format: { 184 | comments: false, 185 | }, 186 | }, 187 | extractComments: false, 188 | }), 189 | new OptimizeCSSAssetsPlugin({ 190 | cssProcessorPluginOptions: { 191 | preset: ['default', {discardComments: {removeAll: true}}], 192 | }, 193 | }), 194 | new FilemanagerPlugin({ 195 | events: { 196 | onEnd: { 197 | archive: [ 198 | { 199 | format: 'zip', 200 | source: path.join(destPath, `${targetBrowser}-rec`), 201 | destination: `${path.join(destPath, targetBrowser)}.rec.${getExtensionFileType(targetBrowser)}`, 202 | options: {zlib: {level: 6}}, 203 | }, 204 | ], 205 | }, 206 | }, 207 | }), 208 | ], 209 | }, 210 | }; --------------------------------------------------------------------------------