diff --git a/.github/workflows/actions/build-test-pack/action.yml b/.github/workflows/actions/build-test-pack/action.yml new file mode 100644 index 00000000..bcb28a0c --- /dev/null +++ b/.github/workflows/actions/build-test-pack/action.yml @@ -0,0 +1,42 @@ +# Assumes that: +# 1. the following env variables are set: +# - ZIP_FILE_PATH +# - EXTENSION_DIR +# 2. repository checked out +# Effects: +# - builds and tests an extension, fails on error +# - packed extension.zip saved to env.ZIP_FILE_PATH if inputs.doNotPackZip == 'false' + +name: "Build, test and pack WebExtension" +description: "Builds, tests, and packs extension dir into zip file" + +inputs: + doNotPackZip: + description: 'Set `true` to omit pack step' + required: false + +runs: + using: "composite" + steps: + # Add additional build and test steps here + + - name: Copy extension to folder + shell: bash + run: | + mkdir -p ${{ env.EXTENSION_DIR }} + cp manifest.json ${{ env.EXTENSION_DIR }} + cp blocked.html ${{ env.EXTENSION_DIR }} + cp -r config/ ${{ env.EXTENSION_DIR }} + cp -r images/ ${{ env.EXTENSION_DIR }} + cp -r options/ ${{ env.EXTENSION_DIR }} + cp -r popup/ ${{ env.EXTENSION_DIR }} + cp -r rules/ ${{ env.EXTENSION_DIR }} + cp -r scripts/ ${{ env.EXTENSION_DIR }} + cp -r styles/ ${{ env.EXTENSION_DIR }} + + - name: Pack directory to zip + if: inputs.doNotPackZip != 'true' + uses: cardinalby/webext-buildtools-pack-extension-dir-action@28fdcac9860fb08555580587cab0d33afe4a341d + with: + extensionDir: ${{ env.EXTENSION_DIR }} + zipFilePath: ${{ env.ZIP_FILE_PATH }} \ No newline at end of file diff --git a/.github/workflows/actions/get-zip-asset/action.yml b/.github/workflows/actions/get-zip-asset/action.yml new file mode 100644 index 00000000..88fb6f17 --- /dev/null +++ b/.github/workflows/actions/get-zip-asset/action.yml @@ -0,0 +1,67 @@ +# Assumes that: +# 1. the following env variables are set: +# - ZIP_ASSET_NAME +# - ZIP_FILE_PATH +# - ZIP_FILE_NAME +# - EXTENSION_DIR +# 2. repository checked out +# Effects: +# - extension.zip saved to env.ZIP_FILE_PATH +# - outputs.releaseUploadUrl is set if ref_type == 'tag' and release exists +# - extension.zip uploaded as build artifact to the job if it wasn't found in release + +name: "Obtain extension.zip asset" +description: "Downloads zip asset from a release (if exists) or builds it from the scratch" +inputs: + githubToken: + description: GitHub token + required: true +outputs: + releaseUploadUrl: + description: Release upload url, if exists + value: ${{ steps.getRelease.outputs.upload_url }} +runs: + using: "composite" + steps: + - name: Get release + id: getRelease + if: github.ref_type == 'tag' + uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada + env: + GITHUB_TOKEN: ${{ inputs.githubToken }} + with: + tag: ${{ github.ref_name }} + doNotFailIfNotFound: true + + - name: Find out zip asset id from assets JSON + if: steps.getRelease.outputs.assets + id: readAssetIdFromRelease + uses: cardinalby/js-eval-action@b34865f1d9cfdf35356013627474857cfe0d5091 + env: + ASSETS_JSON: ${{ steps.getRelease.outputs.assets }} + ASSET_NAME: ${{ env.ZIP_ASSET_NAME }} + with: + expression: | + JSON.parse(env.ASSETS_JSON) + .find(asset => asset.name == env.ZIP_ASSET_NAME)?.id || '' + + - name: Download found zip release asset + id: downloadZipAsset + if: steps.readAssetIdFromRelease.outputs.result + uses: cardinalby/download-release-asset-action@8fe4ec3a876fe25b72086c8de1faddfaeb6512ff + with: + token: ${{ inputs.githubToken }} + assetId: ${{ steps.readAssetIdFromRelease.outputs.result }} + targetPath: ${{ env.ZIP_FILE_PATH }} + + - name: Build and pack zip + id: buildZip + if: steps.downloadZipAsset.outcome != 'success' + uses: ./.github/workflows/actions/build-test-pack + + - name: Upload zip file artifact + if: steps.buildZip.outcome == 'success' + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: ${{ env.ZIP_FILE_NAME }} + path: ${{ env.ZIP_FILE_PATH }} \ No newline at end of file diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml new file mode 100644 index 00000000..e5414762 --- /dev/null +++ b/.github/workflows/build-and-test.yml @@ -0,0 +1,32 @@ +name: Build and test +on: + pull_request: + push: + branches: + - 'main' + - 'dev' + workflow_dispatch: +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Build, test and pack to zip + id: build + uses: ./.github/workflows/actions/build-test-pack + with: + # pack zip only for pull requests or workflow_dispatch events + doNotPackZip: ${{ github.event_name == 'push' && 'true' || 'false'}} + + - name: Upload zip file artifact + if: github.event_name != 'push' + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: ${{ env.ZIP_FILE_NAME }} + path: ${{ env.ZIP_FILE_PATH }} \ No newline at end of file diff --git a/.github/workflows/build-assets-on-release.yml b/.github/workflows/build-assets-on-release.yml new file mode 100644 index 00000000..351a95d9 --- /dev/null +++ b/.github/workflows/build-assets-on-release.yml @@ -0,0 +1,96 @@ +# On release published: +# - if no built extension.zip asset attached to release, does that +# - builds and attaches signed crx asset to release +# - builds and attaches signed xpi asset to release +name: Build release assets + +on: + release: + # Creating draft releases will not trigger it + types: [published] +jobs: + # Find out asset id of existing extension.zip asset in a release or + # build and attach it to the release and use its asset id + ensure-zip: + runs-on: ubuntu-latest + permissions: + contents: write + outputs: + zipAssetId: | + ${{ steps.getZipAssetId.outputs.result || + steps.uploadZipAsset.outputs.id }} + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Find out "extension.zip" asset id from the release + id: getZipAssetId + uses: cardinalby/js-eval-action@b34865f1d9cfdf35356013627474857cfe0d5091 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ASSETS_URL: ${{ github.event.release.assets_url }} + ASSET_NAME: ${{ env.ZIP_FILE_NAME }} + with: + expression: | + (await octokit.request("GET " + env.ASSETS_URL)).data + .find(asset => asset.name == env.ASSET_NAME)?.id || '' + + - name: Build, test and pack + if: '!steps.getZipAssetId.outputs.result' + id: buildPack + uses: ./.github/workflows/actions/build-test-pack + + - name: Upload "extension.zip" asset to the release + id: uploadZipAsset + if: '!steps.getZipAssetId.outputs.result' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ${{ env.ZIP_FILE_PATH }} + asset_name: ${{ env.ZIP_FILE_NAME }} + asset_content_type: application/zip + + build-signed-crx-asset: + needs: ensure-zip + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Download zip release asset + uses: cardinalby/download-release-asset-action@8fe4ec3a876fe25b72086c8de1faddfaeb6512ff + with: + token: ${{ secrets.GITHUB_TOKEN }} + assetId: ${{ needs.ensure-zip.outputs.zipAssetId }} + targetPath: ${{ env.ZIP_FILE_PATH }} + + - name: Build offline crx + id: buildOfflineCrx + uses: cardinalby/webext-buildtools-chrome-crx-action@200e7173cbdb5acb91d381cf9f7a30080b025047 + with: + zipFilePath: ${{ env.ZIP_FILE_PATH }} + crxFilePath: ${{ env.OFFLINE_CRX_FILE_PATH }} + privateKey: ${{ secrets.CHROME_CRX_PRIVATE_KEY }} + + - name: Upload offline crx release asset + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ github.event.release.upload_url }} + asset_path: ${{ env.OFFLINE_CRX_FILE_PATH }} + asset_name: ${{ env.OFFLINE_CRX_FILE_NAME }} + asset_content_type: application/x-chrome-extension + diff --git a/.github/workflows/constants.env b/.github/workflows/constants.env new file mode 100644 index 00000000..331cd5fa --- /dev/null +++ b/.github/workflows/constants.env @@ -0,0 +1,11 @@ +EXTENSION_DIR=extension/ +BUILD_DIR=build/ + +ZIP_FILE_NAME=extension.zip +ZIP_FILE_PATH=${BUILD_DIR}${ZIP_FILE_NAME} + +WEBSTORE_CRX_FILE_NAME=extension.webstore.crx +WEBSTORE_CRX_FILE_PATH=${BUILD_DIR}${WEBSTORE_CRX_FILE_NAME} + +OFFLINE_CRX_FILE_NAME=extension.offline.crx +OFFLINE_CRX_FILE_PATH=${BUILD_DIR}${OFFLINE_CRX_FILE_NAME} diff --git a/.github/workflows/google-refresh-token.yml b/.github/workflows/google-refresh-token.yml new file mode 100644 index 00000000..32f188c0 --- /dev/null +++ b/.github/workflows/google-refresh-token.yml @@ -0,0 +1,14 @@ +name: Google Refresh Token +on: + schedule: + - cron: '0 3 2 * *' # At 03:00 on day-of-month 2 + workflow_dispatch: +jobs: + fetchToken: + runs-on: ubuntu-latest + steps: + - uses: cardinalby/google-api-fetch-token-action@24c99245e2a2494cc4c4b1037203d319a184b15b + with: + clientId: ${{ secrets.G_CLIENT_ID }} + clientSecret: ${{ secrets.G_CLIENT_SECRET }} + refreshToken: ${{ secrets.G_REFRESH_TOKEN }} diff --git a/.github/workflows/pr_check.yml b/.github/workflows/pr_check.yml new file mode 100644 index 00000000..a9b71b99 --- /dev/null +++ b/.github/workflows/pr_check.yml @@ -0,0 +1,59 @@ +name: PR Branch Check + +on: + # Using pull_request_target instead of pull_request for secure handling of fork PRs + pull_request_target: + # Only run on these PR events + types: [opened, synchronize, reopened] + # Only check PRs targeting these branches + branches: + - main + - master + +permissions: + pull-requests: write + issues: write + +jobs: + check-branch: + runs-on: ubuntu-latest + steps: + - name: Check and Comment on PR + # Only process fork PRs with specific branch conditions + # Must be a fork AND (source is main/master OR target is main/master) + if: | + github.event.pull_request.head.repo.fork == true && + ((github.event.pull_request.head.ref == 'main' || github.event.pull_request.head.ref == 'master') || + (github.event.pull_request.base.ref == 'main' || github.event.pull_request.base.ref == 'master')) + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + let message = ''; + + // Check if PR is targeting main/master + if (context.payload.pull_request.base.ref === 'main' || context.payload.pull_request.base.ref === 'master') { + message += 'âš ī¸ PRs cannot target the main branch directly. If you are attempting to contribute code please PR to the dev branch.\n\n'; + } + + // Check if PR is from a fork's main/master branch + if (context.payload.pull_request.head.repo.fork && + (context.payload.pull_request.head.ref === 'main' || context.payload.pull_request.head.ref === 'master')) { + message += 'âš ī¸ This PR cannot be merged because it originates from your fork\'s main/master branch. If you are attempting to contribute code please PR from your dev branch or another non-main/master branch.\n\n'; + } + + message += '🔒 This PR will now be automatically closed due to the above rules.'; + + // Post the comment + await github.rest.issues.createComment({ + ...context.repo, + issue_number: context.issue.number, + body: message + }); + + // Close the PR + await github.rest.pulls.update({ + ...context.repo, + pull_number: context.issue.number, + state: 'closed' + }); diff --git a/.github/workflows/publish-on-chrome-webstore.yml b/.github/workflows/publish-on-chrome-webstore.yml new file mode 100644 index 00000000..a9902dfc --- /dev/null +++ b/.github/workflows/publish-on-chrome-webstore.yml @@ -0,0 +1,142 @@ +name: publish-on-chrome-web-store +on: + workflow_dispatch: + inputs: + attemptNumber: + description: 'Attempt number' + required: false + default: '1' + maxAttempts: + description: 'Max attempts' + required: false + default: '10' + environment: + description: 'publish-on-webstore job environment' + required: false + default: '' +jobs: + publish-on-webstore: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment }} + outputs: + result: ${{ steps.webStorePublish.outcome }} + releaseUploadUrl: ${{ steps.getZipAsset.outputs.releaseUploadUrl }} + steps: + - name: Get the next attempt number + id: getNextAttemptNumber + uses: cardinalby/js-eval-action@b34865f1d9cfdf35356013627474857cfe0d5091 + env: + attemptNumber: ${{ github.event.inputs.attemptNumber }} + maxAttempts: ${{ github.event.inputs.maxAttempts }} + with: + expression: | + { + const + attempt = parseInt(env.attemptNumber), + max = parseInt(env.maxAttempts); + assert(attempt && max && max >= attempt); + return attempt < max ? attempt + 1 : ''; + } + + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Obtain packed zip + id: getZipAsset + uses: ./.github/workflows/actions/get-zip-asset + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + + - name: Fetch Google API access token + id: fetchAccessToken + uses: cardinalby/google-api-fetch-token-action@24c99245e2a2494cc4c4b1037203d319a184b15b + with: + clientId: ${{ secrets.G_CLIENT_ID }} + clientSecret: ${{ secrets.G_CLIENT_SECRET }} + refreshToken: ${{ secrets.G_REFRESH_TOKEN }} + + - name: Upload to Google Web Store + id: webStoreUpload + continue-on-error: true + uses: cardinalby/webext-buildtools-chrome-webstore-upload-action@8db7a005529498d95d3e2e0166f6f4050d2b96a5 + with: + zipFilePath: ${{ env.ZIP_FILE_PATH }} + extensionId: ${{ secrets.G_EXTENSION_ID }} + apiAccessToken: ${{ steps.fetchAccessToken.outputs.accessToken }} + waitForUploadCheckCount: 10 + waitForUploadCheckIntervalMs: 180000 # 3 minutes + + # Schedule a next attempt if store refused to accept new version because it + # still has a previous one in review + - name: Start the next attempt with the delay + uses: aurelien-baudet/workflow-dispatch@93e95b157d791ae7f42aef8f8a0d3d723eba1c31 # pin@v2 + if: | + steps.getNextAttemptNumber.outputs.result && + steps.webStoreUpload.outputs.inReviewError == 'true' + with: + workflow: ${{ github.workflow }} + token: ${{ secrets.WORKFLOWS_TOKEN }} + wait-for-completion: false + inputs: | + { + "attemptNumber": "${{ steps.getNextAttemptNumber.outputs.result }}", + "maxAttempts": "${{ github.event.inputs.maxAttempts }}", + "environment": "12hoursDelay" + } + + - name: Abort on unrecoverable upload error + if: | + !steps.webStoreUpload.outputs.newVersion && + steps.webStoreUpload.outputs.sameVersionAlreadyUploadedError != 'true' + run: exit 1 + + - name: Publish on Google Web Store + id: webStorePublish + if: | + steps.webStoreUpload.outputs.newVersion || + steps.webStoreUpload.outputs.sameVersionAlreadyUploadedError == 'true' + uses: cardinalby/webext-buildtools-chrome-webstore-publish-action@d39ebd4ab4ea4b44498bf5fc34d4b3db7706f1ed + with: + extensionId: ${{ secrets.G_EXTENSION_ID }} + apiAccessToken: ${{ steps.fetchAccessToken.outputs.accessToken }} + + download-published-crx: + needs: publish-on-webstore + if: needs.publish-on-webstore.outputs.result == 'success' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Download published crx file + id: gWebStoreDownloadCrx + uses: cardinalby/webext-buildtools-chrome-webstore-download-crx-action@7de7ffb52fac6255a343c5e982871eb23cbee253 + with: + extensionId: ${{ secrets.G_EXTENSION_ID }} + crxFilePath: ${{ env.WEBSTORE_CRX_FILE_PATH }} + + - name: Upload webstore published crx release asset + if: needs.publish-on-webstore.outputs.releaseUploadUrl + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ needs.publish-on-webstore.outputs.releaseUploadUrl }} + asset_path: ${{ env.WEBSTORE_CRX_FILE_PATH }} + asset_name: ${{ env.WEBSTORE_CRX_FILE_NAME }} + asset_content_type: application/x-chrome-extension + + - name: Upload webstore crx file artifact to workflow + if: '!needs.publish-on-webstore.outputs.releaseUploadUrl' + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 + with: + name: ${{ env.WEBSTORE_CRX_FILE_NAME }} + path: ${{ env.WEBSTORE_CRX_FILE_PATH }} diff --git a/.github/workflows/publish-on-edge-add-ons.yml b/.github/workflows/publish-on-edge-add-ons.yml new file mode 100644 index 00000000..ddce9b18 --- /dev/null +++ b/.github/workflows/publish-on-edge-add-ons.yml @@ -0,0 +1,27 @@ +name: publish-on-edge-add-ons +on: + workflow_dispatch: +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Obtain packed zip + uses: ./.github/workflows/actions/get-zip-asset + with: + githubToken: ${{ secrets.GITHUB_TOKEN }} + + - name: Deploy to Edge Addons + uses: wdzeng/edge-addon@fe088a3bb9bf7c3f1cab08df6269664b9f7bf4fd # pin@v1.0.3 + with: + product-id: ${{ secrets.EDGE_PRODUCT_ID }} + zip-path: ${{ env.ZIP_FILE_PATH }} + client-id: ${{ secrets.EDGE_CLIENT_ID }} + client-secret: ${{ secrets.EDGE_CLIENT_SECRET }} + access-token-url: ${{ secrets.EDGE_ACCESS_TOKEN_URL }} \ No newline at end of file diff --git a/.github/workflows/publish-release-on-tag.yml b/.github/workflows/publish-release-on-tag.yml new file mode 100644 index 00000000..79b86e99 --- /dev/null +++ b/.github/workflows/publish-release-on-tag.yml @@ -0,0 +1,71 @@ +name: Release and publish on tag +on: + push: + tags: + - '*.*.*' + workflow_dispatch: +jobs: + build-release-publish: + if: github.ref_type == 'tag' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: cardinalby/export-env-action@66657b34899a2d695434ed060d9f2215db9b4035 + with: + envFile: './.github/workflows/constants.env' + expand: true + + - name: Look for existing release + id: getRelease + uses: cardinalby/git-get-release-action@cedef2faf69cb7c55b285bad07688d04430b7ada + continue-on-error: true + with: + tag: ${{ github.ref_name }} + env: + GITHUB_TOKEN: ${{ github.token }} + + - name: Build, test and pack to zip + id: buildPack + if: steps.getRelease.outcome != 'success' + uses: ./.github/workflows/actions/build-test-pack + + - name: Create Release + id: createRelease + if: steps.getRelease.outcome != 'success' + uses: ncipollo/release-action@58ae73b360456532aafd58ee170c045abbeaee37 # pin@v1 + with: + token: ${{ secrets.GITHUB_TOKEN }} + draft: 'true' + + - name: Upload zip asset to the release + if: steps.getRelease.outcome != 'success' + uses: actions/upload-release-asset@v1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + upload_url: ${{ steps.createRelease.outputs.upload_url }} + asset_path: ${{ env.ZIP_FILE_PATH }} + asset_name: ${{ env.ZIP_FILE_NAME }} + asset_content_type: application/zip + + # Should trigger build-assets-on-release.yml + - name: Publish release + if: steps.getRelease.outcome != 'success' + uses: eregon/publish-release@c2c0552ef2dd8209aea2a95c940a156eb8f6e9c1 # pin@v1 + env: + GITHUB_TOKEN: ${{ secrets.WORKFLOWS_TOKEN }} + with: + release_id: ${{ steps.createRelease.outputs.id }} + + - name: Publish on Chrome Webstore + uses: benc-uk/workflow-dispatch@4c044c1613fabbe5250deadc65452d54c4ad4fc7 # pin@v1 + with: + workflow: publish-on-chrome-web-store + token: ${{ secrets.WORKFLOWS_TOKEN }} + + - name: Publish on Edge Add-ons + uses: benc-uk/workflow-dispatch@4c044c1613fabbe5250deadc65452d54c4ad4fc7 # pin@v1 + with: + workflow: publish-on-edge-add-ons + token: ${{ secrets.WORKFLOWS_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index bf5d07ac..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,236 +0,0 @@ -name: Build and Release Extension - -on: - push: - tags: - - 'v*' # Trigger on version tags like v1.0.0 - workflow_dispatch: # Allow manual trigger - inputs: - version: - description: 'Version number (e.g., 1.0.0)' - required: true - default: '1.0.0' - publish_to_store: - description: 'Publish to Chrome Web Store' - type: boolean - default: false - -env: - EXTENSION_ID: 'benimdeioplgkhanklclahllklceahbe' - -jobs: - build-and-release: - runs-on: ubuntu-latest - - outputs: - version: ${{ steps.version.outputs.version }} - package-name: ${{ steps.package.outputs.name }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '18' - - - name: Get version - id: version - run: | - if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - VERSION="${{ github.event.inputs.version }}" - else - VERSION=${GITHUB_REF#refs/tags/v} - fi - echo "version=${VERSION}" >> $GITHUB_OUTPUT - echo "Version: $VERSION" - - - name: Update manifest version - run: | - VERSION="${{ steps.version.outputs.version }}" - # Update manifest.json version - sed -i "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" manifest.json - echo "Updated manifest version to $VERSION" - - # Verify the change - grep "version" manifest.json - - - name: Disable development mode in options.js - run: | - # Ensure production build - sed -i 's/const DEVELOPMENT_MODE = true/const DEVELOPMENT_MODE = false/g' options/options.js - echo "✅ Development mode disabled for production" - - - name: Create extension package - id: package - run: | - VERSION="${{ steps.version.outputs.version }}" - PACKAGE_NAME="check-extension-v${VERSION}" - - # Use our existing packaging script logic - mkdir -p extension-build - - # Copy only necessary files for store submission - cp manifest.json extension-build/ - cp blocked.html extension-build/ - cp -r config/ extension-build/ - cp -r images/ extension-build/ - cp -r options/ extension-build/ - cp -r popup/ extension-build/ - cp -r rules/ extension-build/ - cp -r scripts/ extension-build/ - cp -r styles/ extension-build/ - - # Remove any development files - find extension-build -name "*.md" -delete - find extension-build -name "*.log" -delete - find extension-build -name ".DS_Store" -delete - find extension-build -name "Thumbs.db" -delete - - # Create the zip package - cd extension-build - zip -r "../${PACKAGE_NAME}.zip" . - cd .. - - echo "name=${PACKAGE_NAME}" >> $GITHUB_OUTPUT - echo "Package created: ${PACKAGE_NAME}.zip" - - - name: Verify package - run: | - PACKAGE_FILE="${{ steps.package.outputs.name }}.zip" - - if [ ! -f "$PACKAGE_FILE" ]; then - echo "❌ Error: Package file not created" - exit 1 - fi - - SIZE=$(ls -lh "$PACKAGE_FILE" | awk '{print $5}') - echo "đŸ“Ļ Package size: $SIZE" - - # Verify manifest is valid JSON - unzip -p "$PACKAGE_FILE" manifest.json | jq . > /dev/null - echo "✅ Manifest JSON is valid" - - echo "📋 Package contents:" - unzip -l "$PACKAGE_FILE" - - - name: Upload to Chrome Web Store - if: ${{ github.event.inputs.publish_to_store == 'true' || github.event_name == 'push' }} - uses: mnao305/chrome-extension-upload@v5.0.0 - with: - file-path: ${{ steps.package.outputs.name }}.zip - extension-id: ${{ env.EXTENSION_ID }} - client-id: ${{ secrets.CHROME_CLIENT_ID }} - client-secret: ${{ secrets.CHROME_CLIENT_SECRET }} - refresh-token: ${{ secrets.CHROME_REFRESH_TOKEN }} - publish: true - - - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.ref_name || format('v{0}', steps.version.outputs.version) }} - name: Check v${{ steps.version.outputs.version }} - body: | - ## đŸ›Ąī¸ Check Extension v${{ steps.version.outputs.version }} - - **Enterprise phishing protection for Microsoft 365** - - ### īŋŊ Installation Options: - - #### Chrome Web Store (Recommended) - - Install directly from: [Chrome Web Store](https://chrome.google.com/webstore/detail/${{ env.EXTENSION_ID }}) - - Automatic updates and security - - #### Manual Installation (Enterprise/Development) - 1. Download `${{ steps.package.outputs.name }}.zip` - 2. Extract to a folder - 3. Open Chrome → `chrome://extensions/` - 4. Enable "Developer mode" - 5. Click "Load unpacked" → Select extracted folder - - ### đŸĸ Enterprise Deployment - - For enterprise managed deployments, use the registry files in the `enterprise/` folder: - - `registry-chrome-store.reg` - For Chrome Web Store installations - - `registry-edge-store.reg` - For Edge Add-ons installations - - Extension IDs: - - Chrome: `${{ env.EXTENSION_ID }}` - - Edge: (pending publication) - - ### ✨ Features: - - ⚡ Real-time phishing detection for Microsoft 365 - - đŸĸ Enterprise policy support (GPO/Intune) - - 🎨 Customizable branding and configuration - - 📊 Comprehensive logging and monitoring - - 🔒 Modern Manifest V3 architecture - - 🌐 Multi-browser support (Chrome, Edge) - - ### 📋 Requirements: - - Chrome/Chromium browser (version 88+) - - Manifest V3 support - - ### 🔧 Enterprise Configuration: - ```bash - # Update registry with store extension ID - .\Update-StoreIDs.ps1 -ChromeID "${{ env.EXTENSION_ID }}" - - # Deploy policies - cd enterprise - .\Deploy-ADMX.ps1 - ``` - - --- - **Built by CyberDrain** | Released $(date '+%Y-%m-%d %H:%M UTC') - files: | - ${{ steps.package.outputs.name }}.zip - draft: false - prerelease: ${{ contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'alpha') }} - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - - name: Upload build artifact - uses: actions/upload-artifact@v4 - with: - name: extension-v${{ steps.version.outputs.version }} - path: ${{ steps.package.outputs.name }}.zip - retention-days: 90 - - update-enterprise-configs: - needs: build-and-release - runs-on: ubuntu-latest - if: ${{ github.event.inputs.publish_to_store == 'true' || github.event_name == 'push' }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Update enterprise registry files - run: | - VERSION="${{ needs.build-and-release.outputs.version }}" - - # Update registry files with current Chrome extension ID - sed -i "s/CHROME_EXTENSION_ID/${{ env.EXTENSION_ID }}/g" enterprise/registry-chrome-store.reg - - echo "✅ Updated enterprise registry files with extension ID: ${{ env.EXTENSION_ID }}" - - # Show updated files - echo "📋 Updated registry files:" - grep -l "${{ env.EXTENSION_ID }}" enterprise/*.reg || true - - - name: Commit updated enterprise configs - run: | - git config --local user.email "action@github.com" - git config --local user.name "GitHub Action" - - if [ -n "$(git status --porcelain)" ]; then - git add enterprise/ - git commit -m "chore: update enterprise configs for v${{ needs.build-and-release.outputs.version }}" - git push - echo "✅ Committed updated enterprise configurations" - else - echo "â„šī¸ No changes to commit" - fi - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/blocked.html b/blocked.html index 5363776d..ead857c7 100644 --- a/blocked.html +++ b/blocked.html @@ -281,10 +281,30 @@ .btn { width: 100%; } + + } + + /* Anti-flicker: Hide content until fully loaded */ + body.loading .container { + opacity: 0; + } + + .container { + opacity: 1; + transition: opacity 0.2s ease-in; } + + .footer { + margin-top: 40px; + padding-top: 20px; + border-top: 1px solid #e5e7eb; + font-size: 12px; + color: #9ca3af; + } + - +
@@ -309,6 +329,9 @@

Access Blocked

+
diff --git a/config/managed_schema.json b/config/managed_schema.json index 96efa860..46a53e75 100644 --- a/config/managed_schema.json +++ b/config/managed_schema.json @@ -70,6 +70,43 @@ "type": "boolean", "default": false }, + "genericWebhook": { + "title": "Generic Webhook", + "description": "Generic webhook configuration for sending detection events to custom endpoint", + "type": "object", + "properties": { + "enabled": { + "title": "Enabled", + "description": "Enable generic webhook", + "type": "boolean", + "default": false + }, + "url": { + "title": "Webhook URL", + "description": "The URL to send webhook payloads to", + "type": "string", + "format": "uri", + "default": "" + }, + "events": { + "title": "Event Types", + "description": "Array of event types to send to this webhook", + "type": "array", + "items": { + "type": "string", + "enum": [ + "detection_alert", + "false_positive_report", + "page_blocked", + "rogue_app_detected", + "threat_detected", + "validation_event" + ] + }, + "default": [] + } + } + }, "customBranding": { "title": "Custom Branding", "description": "Custom branding configuration for white labeling", diff --git a/docs/deployment/chrome-edge-deployment-instructions/macos.md b/docs/deployment/chrome-edge-deployment-instructions/macos.md index d6165437..0b65f0cc 100644 --- a/docs/deployment/chrome-edge-deployment-instructions/macos.md +++ b/docs/deployment/chrome-edge-deployment-instructions/macos.md @@ -4,4 +4,4 @@ icon: apple # MacOS -Coming soon. If you have experience deploying managed MacOS browser extensions, please contribute to the [docs via GitHub](https://github.com/CyberDrain/Check/tree/main/docs). All Mac resources in the GitHub repo should be considered inaccurate until tested. +Coming soon. If you have experience deploying managed MacOS browser extensions, please contribute to the [docs via GitHub](https://github.com/CyberDrain/Check/tree/dev/docs). All Mac resources in the GitHub repo should be considered inaccurate until tested. diff --git a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md index 49d174f4..1a651c61 100644 --- a/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md +++ b/docs/deployment/chrome-edge-deployment-instructions/windows/domain-deployment.md @@ -30,9 +30,9 @@ Documentation to follow 1. Download the following from the Check repo on GitHub - 1. ​[Deploy-ADMX.ps1](../../../../enterprise/Deploy-ADMX.ps1) - 2. ​[Check-Extension.admx](../../../../enterprise/admx/Check-Extension.admx)​ - 3. ​[Check-Extension.adml](../../../../enterprise/admx/en-US/Check-Extension.adml)​ + 1. ​[Deploy-ADMX.ps1](https://github.com/CyberDrain/Check/blob/main/enterprise/Deploy-ADMX.ps1) + 2. ​[Check-Extension.admx](https://github.com/CyberDrain/Check/blob/main/enterprise/admx/Check-Extension.admx)​ + 3. ​[Check-Extension.adml](https://github.com/CyberDrain/Check/blob/main/enterprise/admx/en-US/Check-Extension.adml)​ 2. Run Deploy-ADMX.ps1. As long as you keep the other two files in the same folder, it will correctly add the available objects to Group Policy. 3. Open Group Policy and create a policy using the imported settings that can be found at `Computer Configuration → Policies → Administrative Templates → CyberDrain → Check - Microsoft 365 Phishing Protection` diff --git a/docs/settings/branding.md b/docs/settings/branding.md index 1b41b5b8..f635c235 100644 --- a/docs/settings/branding.md +++ b/docs/settings/branding.md @@ -11,7 +11,7 @@ Most individual users can skip this section unless they want to personalize the ## Company Information {% hint style="warning" %} -### What if Settings Are Not Visible? +#### What if Settings Are Not Visible? If some settings do not appear on your version, it means your organization's IT department has set these for you. This is normal in business environments - your IT team wants to make sure everyone has the same security settings. You will also see text indicating that the extension is being managed by policy. {% endhint %} diff --git a/docs/settings/general.md b/docs/settings/general.md index 9a55cedd..de261b56 100644 --- a/docs/settings/general.md +++ b/docs/settings/general.md @@ -30,9 +30,140 @@ Currently, CIPP displays these alerts in the logbook. Future updates to CIPP are You can monitor CIPP reporting status and activity in [Activity Logs](activity-logs.md). {% endhint %} +### **False Positive Webhook URL** + +This setting allows you to configure a webhook endpoint that receives false positive reports from users. When configured, a "Report False Positive" button will appear on blocked pages, allowing users to report when Check has incorrectly blocked a legitimate website. + +Enter the full URL to your webhook endpoint (e.g., `https://your-server.com/api/false-positive`). When a user clicks the "Report False Positive" button, Check will send a POST request with comprehensive detection data to help you review and improve your detection rules. + +#### Webhook Payload Structure + +Your webhook endpoint will receive a POST request with `Content-Type: application/json` containing the following fields: + +| Field | Type | Description | +|-------|------|-------------| +| `timestamp` | string | ISO 8601 timestamp when the report was submitted | +| `reportType` | string | Always "false_positive" | +| `blockedUrl` | string | The defanged URL that was blocked (colons replaced with `[:]`) | +| `blockReason` | string | User-facing explanation for why the page was blocked | +| `userAgent` | string | Complete browser user agent string | +| `browserInfo` | object | Browser environment details (see below) | +| `screenResolution` | object | Display information (see below) | +| `detectionDetails` | object | Complete detection data (see below) | +| `extensionVersion` | string | Version of Check that generated the report | + +**browserInfo object:** +- `platform` - Operating system (e.g., "Linux x86_64", "Win32", "MacIntel") +- `language` - Browser language setting (e.g., "en-US") +- `vendor` - Browser vendor (e.g., "Google Inc.") +- `cookiesEnabled` - Boolean indicating if cookies are enabled +- `onLine` - Boolean indicating network connectivity status + +**screenResolution object:** +- `width` - Screen width in pixels +- `height` - Screen height in pixels +- `availWidth` - Available screen width (excluding taskbars) +- `availHeight` - Available screen height (excluding taskbars) +- `colorDepth` - Color depth in bits (e.g., 24) + +**detectionDetails object:** +- `url` - Original URL (non-defanged) +- `score` - Legitimacy score assigned by detection engine +- `threshold` - Threshold value that triggered the block +- `reason` - Detailed technical reason for blocking +- `pageTitle` - Title of the blocked page +- `timestamp` - When the page was blocked +- `threats` - Array of threat objects with `id`, `type`, `description`, and `severity` +- `phishingIndicators` - Array of specific indicators that triggered detection +- Additional fields depending on detection method used + +#### Complete Payload Example + +```json +{ + "timestamp": "2025-11-05T21:30:00.000Z", + "reportType": "false_positive", + "blockedUrl": "https[:]//example[.]com/login", + "blockReason": "This website looks like it has tried to steal your login credentials, to prevent you from logging in we've blocked access.", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", + "browserInfo": { + "platform": "Linux x86_64", + "language": "en-US", + "vendor": "Google Inc.", + "cookiesEnabled": true, + "onLine": true + }, + "screenResolution": { + "width": 1920, + "height": 1080, + "availWidth": 1920, + "availHeight": 1040, + "colorDepth": 24 + }, + "detectionDetails": { + "url": "https://example.com/login", + "score": 42, + "threshold": 50, + "reason": "Multiple phishing indicators detected: score 42/50 (3 phishing indicators)", + "pageTitle": "Sign In - Example Services", + "timestamp": "2025-11-05T21:29:45.000Z", + "userAgent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + "threats": [ + { + "id": "phi_suspicious_domain", + "type": "domain_analysis", + "description": "Domain closely resembles microsoft.com", + "severity": "high" + }, + { + "id": "phi_fake_login_form", + "type": "form_analysis", + "description": "Login form mimics Microsoft 365 sign-in", + "severity": "medium" + } + ], + "phishingIndicators": [ + { + "id": "phi_suspicious_domain", + "description": "Domain closely resembles microsoft.com", + "severity": "high" + }, + { + "id": "phi_fake_login_form", + "description": "Login form mimics Microsoft 365 sign-in", + "severity": "medium" + }, + { + "id": "phi_suspicious_title", + "description": "Page title suggests Microsoft login", + "severity": "low" + } + ] + }, + "extensionVersion": "1.0.0" +} +``` + +#### Webhook Requirements + +Your webhook endpoint should: +1. Accept POST requests with `Content-Type: application/json` +2. Respond with HTTP status codes: + - `200 OK` - Report successfully received + - `4xx` - Client error (user will see error message) + - `5xx` - Server error (user will see error message) +3. Respond within 30 seconds to avoid timeout +4. Use HTTPS to protect sensitive detection data in transit + +{% hint style="info" %} +**Usage Notes:** +- Leave this field empty if you don't want to enable false positive reporting +- The "Report False Positive" button only appears when this webhook URL is configured +{% endhint %} + ## User Interface -### **Show Notifications** +### **Show Notifications** When Check blocks a dangerous website or finds something suspicious, it can show you a small popup message to let you know what's going on. We recommend leaving this setting enabled @@ -41,8 +172,7 @@ When Check blocks a dangerous website or finds something suspicious, it can show This adds a small green checkmark to real Microsoft login pages. This feature is optional. {% hint style="warning" %} - -### What if Settings Are Not Visible? +#### What if Settings Are Not Visible? If some settings do not appear on my version, it means your organization's IT department has set these for you. This is normal in business environments - your IT team wants to make sure everyone has the same security settings. You will also see text indicating that the extension is being managed by policy. {% endhint %} diff --git a/docs/webhooks.md b/docs/webhooks.md new file mode 100644 index 00000000..fc11d03a --- /dev/null +++ b/docs/webhooks.md @@ -0,0 +1,268 @@ +# Webhook System + +## Configuration + +Configure a single generic webhook that can receive multiple event types: + +```json +{ + "genericWebhook": { + "enabled": true, + "url": "https://webhook.example.com/endpoint", + "events": [ + "detection_alert", + "false_positive_report", + "page_blocked", + "rogue_app_detected" + ] + } +} +``` + +CIPP reporting uses separate dedicated settings: + +```json +{ + "enableCippReporting": true, + "cippServerUrl": "https://cipp-server.com", + "cippTenantId": "tenant-id" +} +``` + +## Unified Webhook Schema + +All webhook payloads follow a consistent structure: + +```json +{ + "version": "1.0", + "type": "", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "user": { /* optional user profile */ }, + "browser": { /* optional browser context */ }, + "tenantId": "tenant-id", + "data": { + "url": "https://example.com", + "severity": "high|medium|low|critical|info", + "score": 0, + "threshold": 85, + "reason": "Description of event", + "detectionMethod": "rules_engine|rogue_app_detection|etc", + "rule": "rule-id", + "ruleDescription": "Rule description", + "category": "phishing|oauth_threat|validation|etc", + "context": { + "referrer": null, + "pageTitle": null, + "domain": null, + "redirectTo": null + } + } +} +``` + +## Webhook Types + +### detection_alert +General phishing detection events. + +```json +{ + "version": "1.0", + "type": "detection_alert", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://phishing-site.example.com", + "severity": "high", + "score": 15, + "threshold": 85, + "reason": "Multiple phishing indicators detected", + "detectionMethod": "rules_engine", + "rule": "rule-1", + "ruleDescription": "Form posts to non-Microsoft domain", + "category": "phishing", + "confidence": 0.9, + "matchedRules": ["rule-1", "rule-2"], + "context": { + "referrer": "https://email-client.com", + "pageTitle": "Microsoft Login", + "domain": "phishing-site.example.com", + "redirectTo": null + } + } +} +``` + +### false_positive_report +User-submitted false positive reports from blocked pages. + +```json +{ + "version": "1.0", + "type": "false_positive_report", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://legitimate-site.example.com", + "severity": "info", + "reason": "User reported false positive", + "reportTimestamp": "2025-11-05T22:00:00.000Z", + "userAgent": "Mozilla/5.0...", + "browserInfo": { + "platform": "Linux x86_64", + "language": "en-US" + }, + "detectionDetails": {}, + "userComments": null, + "context": { + "referrer": null, + "pageTitle": null, + "domain": null + } + } +} +``` + +### page_blocked +Sent when a page is blocked. + +```json +{ + "version": "1.0", + "type": "page_blocked", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://malicious-site.example.com", + "severity": "critical", + "score": 0, + "threshold": 85, + "reason": "Phishing attempt detected", + "detectionMethod": "rules_engine", + "rule": "critical-rule-id", + "ruleDescription": "Critical phishing indicator detected", + "category": "phishing", + "action": "blocked", + "context": { + "referrer": null, + "pageTitle": "Fake Login", + "domain": "malicious-site.example.com", + "redirectTo": null + } + } +} +``` + +### rogue_app_detected +OAuth rogue application detection events. + +```json +{ + "version": "1.0", + "type": "rogue_app_detected", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://login.microsoftonline.com/...", + "severity": "critical", + "reason": "Rogue OAuth application detected", + "detectionMethod": "rogue_app_detection", + "category": "oauth_threat", + "clientId": "app-client-id", + "appName": "Suspicious App", + "appInfo": { + "description": "Known malicious OAuth application", + "tags": ["BEC", "exfiltration"], + "references": ["https://..."], + "risk": "high" + }, + "context": { + "referrer": null, + "pageTitle": null, + "domain": null, + "redirectTo": "https://malicious-redirect.com", + "isLocalhost": false, + "isPrivateIP": false + } + } +} +``` + +### threat_detected +General threat detection events. + +```json +{ + "version": "1.0", + "type": "threat_detected", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://suspicious-site.example.com", + "severity": "medium", + "score": 50, + "threshold": 85, + "reason": "Suspicious content detected", + "detectionMethod": "content_analysis", + "rule": null, + "category": "credential_harvesting", + "confidence": 0.75, + "indicators": ["fake-login-form", "typosquatting"], + "matchedRules": ["rule-a", "rule-b"], + "context": { + "referrer": null, + "pageTitle": "Login", + "domain": "suspicious-site.example.com", + "redirectTo": null + } + } +} +``` + +### validation_event +Legitimate page validation events. + +```json +{ + "version": "1.0", + "type": "validation_event", + "timestamp": "2025-11-05T22:00:00.000Z", + "source": "Check Extension", + "extensionVersion": "1.0.0", + "data": { + "url": "https://login.microsoftonline.com", + "severity": "info", + "reason": "Legitimate domain validated", + "detectionMethod": "domain_validation", + "category": "validation", + "result": "legitimate", + "confidence": 1.0, + "context": { + "referrer": null, + "pageTitle": null, + "domain": "login.microsoftonline.com", + "redirectTo": null + } + } +} +``` + +## HTTP Headers + +All webhook requests include: + +``` +Content-Type: application/json +User-Agent: Check/{version} +X-Webhook-Type: {webhook-type} +X-Webhook-Version: 1.0 +``` + diff --git a/enterprise/Remove-Windows-Chrome-and-Edge.ps1 b/enterprise/Remove-Windows-Chrome-and-Edge.ps1 new file mode 100644 index 00000000..fed3c9d9 --- /dev/null +++ b/enterprise/Remove-Windows-Chrome-and-Edge.ps1 @@ -0,0 +1,139 @@ +# Define extension details (same as install-check.ps1) +# Chrome +$chromeExtensionId = "benimdeioplgkhanklclahllklceahbe" +$chromeManagedStorageKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\$chromeExtensionId\policy" +$chromeExtensionSettingsKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\ExtensionSettings\$chromeExtensionId" + +# Edge +$edgeExtensionId = "knepjpocdagponkonnbggpcnhnaikajg" +$edgeManagedStorageKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\$edgeExtensionId\policy" +$edgeExtensionSettingsKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\ExtensionSettings\$edgeExtensionId" + +# Function to remove extension settings +function Remove-ExtensionSettings { + param ( + [string]$ExtensionId, + [string]$ManagedStorageKey, + [string]$ExtensionSettingsKey + ) + + # Remove properties from managed storage key + if (Test-Path $ManagedStorageKey) { + $propertiesToRemove = @( + "showNotifications", + "enableValidPageBadge", + "enablePageBlocking", + "enableCippReporting", + "cippServerUrl", + "cippTenantId", + "customRulesUrl", + "updateInterval", + "enableDebugLogging" + ) + + foreach ($property in $propertiesToRemove) { + if (Get-ItemProperty -Path $ManagedStorageKey -Name $property -ErrorAction SilentlyContinue) { + Remove-ItemProperty -Path $ManagedStorageKey -Name $property -Force -ErrorAction SilentlyContinue + Write-Host "Removed property: $property from $ManagedStorageKey" + } + } + + # Remove URL allowlist subkey and all its properties + $urlAllowlistKey = "$ManagedStorageKey\urlAllowlist" + if (Test-Path $urlAllowlistKey) { + # Remove all numbered properties (1, 2, 3, etc.) + $properties = Get-ItemProperty -Path $urlAllowlistKey -ErrorAction SilentlyContinue + if ($properties) { + $properties.PSObject.Properties | Where-Object { $_.Name -match '^\d+$' } | ForEach-Object { + Remove-ItemProperty -Path $urlAllowlistKey -Name $_.Name -Force -ErrorAction SilentlyContinue + Write-Host "Removed URL allowlist property: $($_.Name) from $urlAllowlistKey" + } + } + # Remove the urlAllowlist subkey if it's empty + try { + Remove-Item -Path $urlAllowlistKey -Force -ErrorAction SilentlyContinue + Write-Host "Removed URL allowlist subkey: $urlAllowlistKey" + } catch { + # Key may not be empty or may have been removed already + } + } + + # Remove custom branding subkey and all its properties + $customBrandingKey = "$ManagedStorageKey\customBranding" + if (Test-Path $customBrandingKey) { + $brandingPropertiesToRemove = @( + "companyName", + "companyURL", + "productName", + "supportEmail", + "primaryColor", + "logoUrl" + ) + + foreach ($property in $brandingPropertiesToRemove) { + if (Get-ItemProperty -Path $customBrandingKey -Name $property -ErrorAction SilentlyContinue) { + Remove-ItemProperty -Path $customBrandingKey -Name $property -Force -ErrorAction SilentlyContinue + Write-Host "Removed custom branding property: $property from $customBrandingKey" + } + } + + # Remove the customBranding subkey if it's empty + try { + Remove-Item -Path $customBrandingKey -Force -ErrorAction SilentlyContinue + Write-Host "Removed custom branding subkey: $customBrandingKey" + } catch { + # Key may not be empty or may have been removed already + } + } + + # Remove the managed storage key if it's empty + try { + $remainingProperties = Get-ItemProperty -Path $ManagedStorageKey -ErrorAction SilentlyContinue + if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) { + Remove-Item -Path $ManagedStorageKey -Force -ErrorAction SilentlyContinue + Write-Host "Removed managed storage key: $ManagedStorageKey" + } + } catch { + # Key may not be empty or may have been removed already + } + } + + # Remove properties from extension settings key + if (Test-Path $ExtensionSettingsKey) { + $extensionPropertiesToRemove = @( + "installation_mode", + "update_url" + ) + + # Add browser-specific toolbar properties + if ($ExtensionId -eq $edgeExtensionId) { + $extensionPropertiesToRemove += "toolbar_state" + } elseif ($ExtensionId -eq $chromeExtensionId) { + $extensionPropertiesToRemove += "toolbar_pin" + } + + foreach ($property in $extensionPropertiesToRemove) { + if (Get-ItemProperty -Path $ExtensionSettingsKey -Name $property -ErrorAction SilentlyContinue) { + Remove-ItemProperty -Path $ExtensionSettingsKey -Name $property -Force -ErrorAction SilentlyContinue + Write-Host "Removed extension setting property: $property from $ExtensionSettingsKey" + } + } + + # Remove the extension settings key if it's empty + try { + $remainingProperties = Get-ItemProperty -Path $ExtensionSettingsKey -ErrorAction SilentlyContinue + if ($remainingProperties -and $remainingProperties.PSObject.Properties.Count -eq 0) { + Remove-Item -Path $ExtensionSettingsKey -Force -ErrorAction SilentlyContinue + Write-Host "Removed extension settings key: $ExtensionSettingsKey" + } + } catch { + # Key may not be empty or may have been removed already + } + } + + Write-Host "Completed removal of extension settings for $ExtensionId" +} + +# Remove settings for Chrome and Edge +Remove-ExtensionSettings -ExtensionId $chromeExtensionId -ManagedStorageKey $chromeManagedStorageKey -ExtensionSettingsKey $chromeExtensionSettingsKey +Remove-ExtensionSettings -ExtensionId $edgeExtensionId -ManagedStorageKey $edgeManagedStorageKey -ExtensionSettingsKey $edgeExtensionSettingsKey diff --git a/enterprise/Test-Extension-Policy.ps1 b/enterprise/Test-Extension-Policy.ps1 new file mode 100644 index 00000000..6a07d23e --- /dev/null +++ b/enterprise/Test-Extension-Policy.ps1 @@ -0,0 +1,121 @@ +# Quick Test Script for Check Extension Registry Settings +# Run as Administrator: Right-click PowerShell -> Run as Administrator +# Then execute: .\Test-Extension-Policy.ps1 + +# Extension IDs +$chromeExtId = "jlpkafnpidpjinmghilbonlgnilmkknn" +$edgeExtId = "jlpkafnpidpjinmghilbonlgnilmkknn" + +# Registry paths +$chromePolicyKey = "HKLM:\SOFTWARE\Policies\Google\Chrome\3rdparty\extensions\$chromeExtId\policy" +$edgePolicyKey = "HKLM:\SOFTWARE\Policies\Microsoft\Edge\3rdparty\extensions\$edgeExtId\policy" + +# Test configuration (modify these values to test different settings) +$testConfig = @{ + showNotifications = 1 + enableValidPageBadge = 1 + enablePageBlocking = 0 + enableCippReporting = 0 + cippServerUrl = "" + cippTenantId = "" + customRulesUrl = "" + updateInterval = 24 + enableDebugLogging = 1 +} + +# Custom branding test values +$testBranding = @{ + companyName = "Test Company" + companyURL = "https://example.com" + productName = "Test Product" + supportEmail = "test@example.com" + primaryColor = "#FF6B00" + logoUrl = "" +} + +function Set-TestPolicies { + param([string]$PolicyKey) + + if (!(Test-Path $PolicyKey)) { + New-Item -Path $PolicyKey -Force | Out-Null + Write-Output "Created policy key: $PolicyKey" + } + + foreach ($key in $testConfig.Keys) { + $value = $testConfig[$key] + $type = if ($value -is [int]) { "DWord" } else { "String" } + New-ItemProperty -Path $PolicyKey -Name $key -PropertyType $type -Value $value -Force | Out-Null + } + + $brandingKey = "$PolicyKey\customBranding" + if (!(Test-Path $brandingKey)) { + New-Item -Path $brandingKey -Force | Out-Null + } + + foreach ($key in $testBranding.Keys) { + New-ItemProperty -Path $brandingKey -Name $key -PropertyType String -Value $testBranding[$key] -Force | Out-Null + } + + Write-Output "Applied test policies to: $PolicyKey" +} + +function Show-CurrentPolicies { + param([string]$PolicyKey) + + if (Test-Path $PolicyKey) { + Write-Output "`nCurrent policies in $PolicyKey" + Get-ItemProperty -Path $PolicyKey | Format-List + + $brandingKey = "$PolicyKey\customBranding" + if (Test-Path $brandingKey) { + Write-Output "`nCustom Branding:" + Get-ItemProperty -Path $brandingKey | Format-List + } + } else { + Write-Output "No policies set at: $PolicyKey" + } +} + +function Remove-TestPolicies { + param([string]$PolicyKey) + + if (Test-Path $PolicyKey) { + Remove-Item -Path $PolicyKey -Recurse -Force + Write-Output "Removed test policies from: $PolicyKey" + } +} + +Write-Output "=== Check Extension Policy Testing Tool ===" +Write-Output "" +Write-Output "1. Apply test policies (Chrome & Edge)" +Write-Output "2. Show current policies" +Write-Output "3. Remove test policies" +Write-Output "4. Exit" +Write-Output "" +$choice = Read-Host "Select option" + +switch ($choice) { + "1" { + Write-Output "`nApplying test policies..." + Set-TestPolicies -PolicyKey $chromePolicyKey + Set-TestPolicies -PolicyKey $edgePolicyKey + Write-Output "`nDone! Restart Chrome/Edge to apply changes." + Write-Output "View policies at: chrome://policy or edge://policy" + } + "2" { + Show-CurrentPolicies -PolicyKey $chromePolicyKey + Show-CurrentPolicies -PolicyKey $edgePolicyKey + } + "3" { + Write-Output "`nRemoving test policies..." + Remove-TestPolicies -PolicyKey $chromePolicyKey + Remove-TestPolicies -PolicyKey $edgePolicyKey + Write-Output "`nDone! Restart Chrome/Edge to clear changes." + } + "4" { + exit + } + default { + Write-Output "Invalid option" + } +} diff --git a/enterprise/admx/en-US/Check-Extension.adml b/enterprise/admx/en-US/Check-Extension.adml index 3b39a3dc..65114ba4 100644 --- a/enterprise/admx/en-US/Check-Extension.adml +++ b/enterprise/admx/en-US/Check-Extension.adml @@ -1,399 +1,466 @@ - - - - - - - CyberDrain - Check - Phishing Protection - Microsoft Edge - Google Chrome - - - Configure Check extension installation (Edge) - This policy configures the installation mode for the Check extension in Microsoft Edge, ensuring it is force-installed and pinned to the toolbar. - -The extension will be: -- Force installed (cannot be removed by users) -- Automatically updated from the Edge Add-ons store -- Pinned to the browser toolbar - -Extension ID: knepjpocdagponkonnbggpcnhnaikajg - - - Configure Check extension installation (Chrome) - This policy configures the installation mode for the Check extension in Google Chrome, ensuring it is force-installed and pinned to the toolbar. - -The extension will be: -- Force installed (cannot be removed by users) -- Automatically updated from the Chrome Web Store -- Pinned to the browser toolbar - -Extension ID: benimdeioplgkhanklclahllklceahbe - - - Display security notifications - This policy controls whether the Check extension displays security notifications and warnings to users. - -When enabled (default): Users will see notifications when phishing attempts are detected or blocked. -When disabled: The extension operates silently without displaying notifications. - - - Display security notifications (Edge) - This policy controls whether the Check extension displays security notifications and warnings to users in Microsoft Edge. - -When enabled (default): Users will see notifications when phishing attempts are detected or blocked. -When disabled: The extension operates silently without displaying notifications. - - - Display security notifications (Chrome) - This policy controls whether the Check extension displays security notifications and warnings to users in Google Chrome. - -When enabled (default): Users will see notifications when phishing attempts are detected or blocked. -When disabled: The extension operates silently without displaying notifications. - - - Show valid page badge - This policy controls whether the Check extension displays a validation badge on legitimate Microsoft login pages. - -When enabled (default): A green badge or indicator shows when users are on a verified Microsoft login page. -When disabled: No visual indicator is shown for valid pages. - - - Enable page blocking - This policy controls whether the Check extension blocks access to detected phishing pages. - -When enabled (default): Suspicious and phishing pages are blocked with a warning screen. -When disabled: Pages are only flagged but not blocked (warning mode only). - - - Enable CIPP reporting - This policy controls whether the Check extension reports security events to a CIPP (CyberDrain Improved Partner Portal) server. - -When enabled: Security events are sent to the configured CIPP server for centralized monitoring. -When disabled (default): No reporting to external servers occurs. - -Note: Requires CIPP Server URL to be configured. - - - CIPP server URL - This policy specifies the base URL for the CIPP server where security events should be reported. - -Example: https://cipp.yourcompany.com - -This setting is only used when CIPP reporting is enabled. The URL should point to a valid CIPP server instance. - - - CIPP tenant identifier - This policy specifies the tenant identifier to include with CIPP alerts for multi-tenant environments. - -Example: contoso.onmicrosoft.com - -This helps identify which tenant/organization the security event originated from when using a shared CIPP instance. - - - Custom detection rules URL - This policy specifies a custom URL from which the extension should fetch detection rules. - -Example: https://yourcompany.com/detection-rules.json - -When specified, the extension will download detection rules from this URL instead of using the default rules. Leave empty to use default detection rules. - -The URL must serve a valid JSON file matching the detection rules schema. - - - Detection rules update interval - This policy specifies how often (in hours) the extension should check for updates to detection rules. - -Default: 24 hours -Range: 1-168 hours (1 hour to 1 week) - -More frequent updates provide better protection but may increase network usage. - - - URL allowlist (URLs with wildcards or regex patterns) - This policy specifies a list of URL patterns or regex patterns to allowlist URLs from detection. - -These patterns will be added to the exclusion rules without replacing the entire ruleset. You can use simple URLs with wildcards or advanced regex patterns. - -Simple URL examples with wildcards: -- https://google.com/* -- https://*.microsoft.com/* -- https://login.microsoftonline.com/* - -Advanced regex examples: -- ^https://trusted\.example\.com/.* -- ^https://.*\.microsoft\.com/.* - -URLs matching any of these patterns will bypass phishing detection. - - - URL allowlist (URLs with wildcards or regex patterns) (Chrome) - This policy specifies a list of URL patterns or regex patterns to allowlist URLs from detection in Google Chrome. - -These patterns will be added to the exclusion rules without replacing the entire ruleset. You can use simple URLs with wildcards or advanced regex patterns. - -Simple URL examples with wildcards: -- https://google.com/* -- https://*.microsoft.com/* -- https://login.microsoftonline.com/* - -Advanced regex examples: -- ^https://trusted\.example\.com/.* -- ^https://.*\.microsoft\.com/.* - -URLs matching any of these patterns will bypass phishing detection. - - - Company name - This policy specifies the company name to display in the extension's user interface for branding purposes. - -Example: Contoso Corporation - -The company name appears in the extension popup and settings pages. - - - Product name - This policy specifies a custom product name for the extension. - -Example: Contoso Security Guard - -This allows organizations to rebrand the extension with their own product name. - - - Support email address - This policy specifies the email address users should contact for support with the extension. - -Example: security@contoso.com - -This email address is displayed in the extension interface and help documentation. - - - Primary theme color - This policy specifies the primary theme color for the extension interface using a hex color code. - -Example: #0078D4 - -The color should be in hex format (e.g., #FF0000 for red, #0078D4 for Microsoft blue). This color is used for UI elements and branding throughout the extension. - - - Company logo URL - This policy specifies the URL to a company logo that will be displayed in the extension interface. - -Example: https://yourcompany.com/logo.png - -The logo should be a square image (recommended size: 48x48 to 128x128 pixels) in PNG, SVG, or other web-compatible format. The logo will be displayed in the extension popup and blocked page. - - - Enable debug logging - This policy controls whether the Check extension generates detailed debug logs for troubleshooting purposes. - -When enabled: Verbose logging is activated and can be viewed in browser developer tools. -When disabled (default): Only essential logging occurs. - -Debug logging should only be enabled for troubleshooting as it may impact performance and generate large log files. - - - Enable debug logging (Chrome) - This policy controls whether the Check extension generates detailed debug logs for troubleshooting purposes in Google Chrome. - -When enabled: Verbose logging is activated and can be viewed in browser developer tools. -When disabled (default): Only essential logging occurs. - -Debug logging should only be enabled for troubleshooting as it may impact performance and generate large log files. - - - Show valid page badge (Chrome) - This policy controls whether the Check extension displays a validation badge on legitimate Microsoft login pages in Google Chrome. - -When enabled (default): A green badge or indicator shows when users are on a verified Microsoft login page. -When disabled: No visual indicator is shown for valid pages. - - - Enable page blocking (Chrome) - This policy controls whether the Check extension blocks access to detected phishing pages in Google Chrome. - -When enabled (default): Suspicious and phishing pages are blocked with a warning screen. -When disabled: Pages are only flagged but not blocked (warning mode only). - - - Enable CIPP reporting (Chrome) - This policy controls whether the Check extension reports security events to a CIPP (CyberDrain Improved Partner Portal) server in Google Chrome. - -When enabled: Security events are sent to the configured CIPP server for centralized monitoring. -When disabled (default): No reporting to external servers occurs. - -Note: Requires CIPP Server URL to be configured. - - - CIPP server URL (Chrome) - This policy specifies the base URL for the CIPP server where security events should be reported from Google Chrome. - -Example: https://cipp.yourcompany.com - -This setting is only used when CIPP reporting is enabled. The URL should point to a valid CIPP server instance. - - - CIPP tenant identifier (Chrome) - This policy specifies the tenant identifier to include with CIPP alerts for multi-tenant environments from Google Chrome. - -Example: contoso.onmicrosoft.com - -This helps identify which tenant/organization the security event originated from when using a shared CIPP instance. - - - Custom detection rules URL (Chrome) - This policy specifies a custom URL from which the extension should fetch detection rules in Google Chrome. - -Example: https://yourcompany.com/detection-rules.json - -When specified, the extension will download detection rules from this URL instead of using the default rules. Leave empty to use default detection rules. - -The URL must serve a valid JSON file matching the detection rules schema. - - - Detection rules update interval (Chrome) - This policy specifies how often (in hours) the extension should check for updates to detection rules in Google Chrome. - -Default: 24 hours -Range: 1-168 hours (1 hour to 1 week) - -More frequent updates provide better protection but may increase network usage. - - - Company name (Chrome) - This policy specifies the company name to display in the extension's user interface for branding purposes in Google Chrome. - -Example: Contoso Corporation - -The company name appears in the extension popup and settings pages. - - - Product name (Chrome) - This policy specifies a custom product name for the extension in Google Chrome. - -Example: Contoso Security Guard - -This allows organizations to rebrand the extension with their own product name. - - - Support email address (Chrome) - This policy specifies the email address users should contact for support with the extension in Google Chrome. - -Example: security@contoso.com - -This email address is displayed in the extension interface and help documentation. - - - Primary theme color (Chrome) - This policy specifies the primary theme color for the extension interface using a hex color code in Google Chrome. - -Example: #0078D4 - -The color should be in hex format (e.g., #FF0000 for red, #0078D4 for Microsoft blue). This color is used for UI elements and branding throughout the extension. - - - Company logo URL (Chrome) - This policy specifies the URL to a company logo that will be displayed in the extension interface in Google Chrome. - -Example: https://yourcompany.com/logo.png - -The logo should be a square image (recommended size: 48x48 to 128x128 pixels) in PNG, SVG, or other web-compatible format. The logo will be displayed in the extension popup and blocked page. - - - - - - - - - - - - - - - - - - - Update Interval (hours): - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Update Interval (hours): - - - - - - - - - - - - - - - - - - - - - - - - - - - - URL Allowlist (URLs with wildcards or regex patterns): - - - URL Allowlist (URLs with wildcards or regex patterns): - - - - + + + + + + + CyberDrain + Check - Phishing Protection + Microsoft Edge + Google Chrome + + Configure Check extension installation (Edge) + + This policy configures the installation mode for the Check extension in Microsoft Edge, ensuring it is force-installed and pinned to the toolbar. + + The extension will be: + - Force installed (cannot be removed by users) + - Automatically updated from the Edge Add-ons store + - Pinned to the browser toolbar + + Extension ID: knepjpocdagponkonnbggpcnhnaikajg + + + Configure Check extension installation (Chrome) + + This policy configures the installation mode for the Check extension in Google Chrome, ensuring it is force-installed and pinned to the toolbar. + + The extension will be: + - Force installed (cannot be removed by users) + - Automatically updated from the Chrome Web Store + - Pinned to the browser toolbar + + Extension ID: benimdeioplgkhanklclahllklceahbe + + + Display security notifications + + This policy controls whether the Check extension displays security notifications and warnings to users. + + When enabled (default): Users will see notifications when phishing attempts are detected or blocked. + When disabled: The extension operates silently without displaying notifications. + + + Display security notifications (Edge) + + This policy controls whether the Check extension displays security notifications and warnings to users in Microsoft Edge. + + When enabled (default): Users will see notifications when phishing attempts are detected or blocked. + When disabled: The extension operates silently without displaying notifications. + + + Display security notifications (Chrome) + + This policy controls whether the Check extension displays security notifications and warnings to users in Google Chrome. + + When enabled (default): Users will see notifications when phishing attempts are detected or blocked. + When disabled: The extension operates silently without displaying notifications. + + + Show valid page badge + + This policy controls whether the Check extension displays a validation badge on legitimate Microsoft login pages. + + When enabled (default): A green badge or indicator shows when users are on a verified Microsoft login page. + When disabled: No visual indicator is shown for valid pages. + + + Enable page blocking + + This policy controls whether the Check extension blocks access to detected phishing pages. + + When enabled (default): Suspicious and phishing pages are blocked with a warning screen. + When disabled: Pages are only flagged but not blocked (warning mode only). + + + Enable CIPP reporting + + This policy controls whether the Check extension reports security events to a CIPP (CyberDrain Improved Partner Portal) server. + + When enabled: Security events are sent to the configured CIPP server for centralized monitoring. + When disabled (default): No reporting to external servers occurs. + + Note: Requires CIPP Server URL to be configured. + + + CIPP server URL + + This policy specifies the base URL for the CIPP server where security events should be reported. + + Example: https://cipp.yourcompany.com + + This setting is only used when CIPP reporting is enabled. The URL should point to a valid CIPP server instance. + + + CIPP tenant identifier + + This policy specifies the tenant identifier to include with CIPP alerts for multi-tenant environments. + + Example: contoso.onmicrosoft.com + + This helps identify which tenant/organization the security event originated from when using a shared CIPP instance. + + + Custom detection rules URL + + This policy specifies a custom URL from which the extension should fetch detection rules. + + Example: https://yourcompany.com/detection-rules.json + + When specified, the extension will download detection rules from this URL instead of using the default rules. Leave empty to use default detection rules. + + The URL must serve a valid JSON file matching the detection rules schema. + + + Detection rules update interval + + This policy specifies how often (in hours) the extension should check for updates to detection rules. + + Default: 24 hours + Range: 1-168 hours (1 hour to 1 week) + + More frequent updates provide better protection but may increase network usage. + + + URL allowlist (URLs with wildcards or regex patterns) + + This policy specifies a list of URL patterns or regex patterns to allowlist URLs from detection. + + These patterns will be added to the exclusion rules without replacing the entire ruleset. You can use simple URLs with wildcards or advanced regex patterns. + + Simple URL examples with wildcards: + - https://google.com/* + - https://*.microsoft.com/* + - https://login.microsoftonline.com/* + + Advanced regex examples: + - ^https://trusted\.example\.com/.* + - ^https://.*\.microsoft\.com/.* + + URLs matching any of these patterns will bypass phishing detection. + + + URL allowlist (URLs with wildcards or regex patterns) (Chrome) + + This policy specifies a list of URL patterns or regex patterns to allowlist URLs from detection in Google Chrome. + + These patterns will be added to the exclusion rules without replacing the entire ruleset. You can use simple URLs with wildcards or advanced regex patterns. + + Simple URL examples with wildcards: + - https://google.com/* + - https://*.microsoft.com/* + - https://login.microsoftonline.com/* + + Advanced regex examples: + - ^https://trusted\.example\.com/.* + - ^https://.*\.microsoft\.com/.* + + URLs matching any of these patterns will bypass phishing detection. + + + Company name + + This policy specifies the company name to display in the extension's user interface for branding purposes. + + Example: Contoso Corporation + + The company name appears in the extension popup and settings pages. + + + Company URL + + This policy specifies the company URL used in the extension for branding and navigation purposes. + + Example: https://contoso.com + + The company URL is used for linking back to the company website from the extension interface. + + + Product name + + This policy specifies a custom product name for the extension. + + Example: Contoso Security Guard + + This allows organizations to rebrand the extension with their own product name. + + + Support email address + + This policy specifies the email address users should contact for support with the extension. + + Example: security@contoso.com + + This email address is displayed in the extension interface and help documentation. + + + Primary theme color + + This policy specifies the primary theme color for the extension interface using a hex color code. + + Example: #0078D4 + + The color should be in hex format (e.g., #FF0000 for red, #0078D4 for Microsoft blue). This color is used for UI elements and branding throughout the extension. + + + Company logo URL + + This policy specifies the URL to a company logo that will be displayed in the extension interface. + + Example: https://yourcompany.com/logo.png + + The logo should be a square image (recommended size: 48x48 to 128x128 pixels) in PNG, SVG, or other web-compatible format. The logo will be displayed in the extension popup and blocked page. + + + Enable debug logging + + This policy controls whether the Check extension generates detailed debug logs for troubleshooting purposes. + + When enabled: Verbose logging is activated and can be viewed in browser developer tools. + When disabled (default): Only essential logging occurs. + + Debug logging should only be enabled for troubleshooting as it may impact performance and generate large log files. + + + Enable debug logging (Chrome) + + This policy controls whether the Check extension generates detailed debug logs for troubleshooting purposes in Google Chrome. + + When enabled: Verbose logging is activated and can be viewed in browser developer tools. + When disabled (default): Only essential logging occurs. + + Debug logging should only be enabled for troubleshooting as it may impact performance and generate large log files. + + + Show valid page badge (Chrome) + + This policy controls whether the Check extension displays a validation badge on legitimate Microsoft login pages in Google Chrome. + + When enabled (default): A green badge or indicator shows when users are on a verified Microsoft login page. + When disabled: No visual indicator is shown for valid pages. + + + Enable page blocking (Chrome) + + This policy controls whether the Check extension blocks access to detected phishing pages in Google Chrome. + + When enabled (default): Suspicious and phishing pages are blocked with a warning screen. + When disabled: Pages are only flagged but not blocked (warning mode only). + + + Enable CIPP reporting (Chrome) + + This policy controls whether the Check extension reports security events to a CIPP (CyberDrain Improved Partner Portal) server in Google Chrome. + + When enabled: Security events are sent to the configured CIPP server for centralized monitoring. + When disabled (default): No reporting to external servers occurs. + + Note: Requires CIPP Server URL to be configured. + + + CIPP server URL (Chrome) + + This policy specifies the base URL for the CIPP server where security events should be reported from Google Chrome. + + Example: https://cipp.yourcompany.com + + This setting is only used when CIPP reporting is enabled. The URL should point to a valid CIPP server instance. + + + CIPP tenant identifier (Chrome) + + This policy specifies the tenant identifier to include with CIPP alerts for multi-tenant environments from Google Chrome. + + Example: contoso.onmicrosoft.com + + This helps identify which tenant/organization the security event originated from when using a shared CIPP instance. + + + Custom detection rules URL (Chrome) + + This policy specifies a custom URL from which the extension should fetch detection rules in Google Chrome. + + Example: https://yourcompany.com/detection-rules.json + + When specified, the extension will download detection rules from this URL instead of using the default rules. Leave empty to use default detection rules. + + The URL must serve a valid JSON file matching the detection rules schema. + + + Detection rules update interval (Chrome) + + This policy specifies how often (in hours) the extension should check for updates to detection rules in Google Chrome. + + Default: 24 hours + Range: 1-168 hours (1 hour to 1 week) + + More frequent updates provide better protection but may increase network usage. + + + Company name (Chrome) + + This policy specifies the company name to display in the extension's user interface for branding purposes in Google Chrome. + + Example: Contoso Corporation + + The company name appears in the extension popup and settings pages. + + + Company URL (Chrome) + + This policy specifies the company URL used in the extension for branding and navigation purposes in Google Chrome. + + Example: https://contoso.com + + The company URL is used for linking back to the company website from the extension interface. + + + Product name (Chrome) + + This policy specifies a custom product name for the extension in Google Chrome. + + Example: Contoso Security Guard + + This allows organizations to rebrand the extension with their own product name. + + + Support email address (Chrome) + + This policy specifies the email address users should contact for support with the extension in Google Chrome. + + Example: security@contoso.com + + This email address is displayed in the extension interface and help documentation. + + + Primary theme color (Chrome) + + This policy specifies the primary theme color for the extension interface using a hex color code in Google Chrome. + + Example: #0078D4 + + The color should be in hex format (e.g., #FF0000 for red, #0078D4 for Microsoft blue). This color is used for UI elements and branding throughout the extension. + + + Company logo URL (Chrome) + + This policy specifies the URL to a company logo that will be displayed in the extension interface in Google Chrome. + + Example: https://yourcompany.com/logo.png + + The logo should be a square image (recommended size: 48x48 to 128x128 pixels) in PNG, SVG, or other web-compatible format. The logo will be displayed in the extension popup and blocked page. + + + + + + + + + + + + + + + + + + + + Update Interval (hours): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Update Interval (hours): + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + URL Allowlist (URLs with wildcards or regex patterns): + + + URL Allowlist (URLs with wildcards or regex patterns): + + + + \ No newline at end of file diff --git a/manifest.json b/manifest.json index a2a1a4e2..d733f8c5 100644 --- a/manifest.json +++ b/manifest.json @@ -20,7 +20,7 @@ }, "content_scripts": [ { - "matches": ["http://*/*", "https://*/*"], + "matches": ["http://*/*", "https://*/*", "file:///*/*"], "js": ["scripts/content.js"], "css": ["styles/content.css"], "run_at": "document_idle", diff --git a/options/options.html b/options/options.html index 8c19a39f..b5c08329 100644 --- a/options/options.html +++ b/options/options.html @@ -115,6 +115,65 @@

Extension Settings

Tenant identifier to include with CIPP alerts for multi-tenant environments

+ +
+ +
+

Generic Webhook

+
+ +

Send events to a custom webhook endpoint

+
+ +
+ +

URL to receive webhook payloads

+
+ +
+ +
+ + + + + + +
+

Select which event types to send to the webhook

+
+
diff --git a/options/options.js b/options/options.js index 732e62f7..66278280 100644 --- a/options/options.js +++ b/options/options.js @@ -886,11 +886,36 @@ class CheckOptions { this.elements.enableValidPageBadge.checked = this.config.enableValidPageBadge || false; - // Detection settings - this.elements.customRulesUrl.value = - this.config?.detectionRules?.customRulesUrl || - this.config?.customRulesUrl || - ""; + // Detection settings - use top-level customRulesUrl consistently + this.elements.customRulesUrl.value = this.config?.customRulesUrl || ""; + + // Generic webhook settings + this.elements.genericWebhookEnabled = document.getElementById("genericWebhookEnabled"); + this.elements.genericWebhookUrl = document.getElementById("genericWebhookUrl"); + + if (this.elements.genericWebhookEnabled) { + this.elements.genericWebhookEnabled.checked = this.config?.genericWebhook?.enabled || false; + } + if (this.elements.genericWebhookUrl) { + this.elements.genericWebhookUrl.value = this.config?.genericWebhook?.url || ""; + } + + const eventTypes = [ + "detection_alert", + "false_positive_report", + "page_blocked", + "rogue_app_detected", + "threat_detected", + "validation_event" + ]; + + const selectedEvents = this.config?.genericWebhook?.events || []; + eventTypes.forEach(eventType => { + const checkbox = document.getElementById(`webhookEvent_${eventType}`); + if (checkbox) { + checkbox.checked = selectedEvents.includes(eventType); + } + }); // URL Allowlist settings if (this.elements.urlAllowlist) { @@ -1115,6 +1140,22 @@ class CheckOptions { // Detection settings customRulesUrl: this.elements.customRulesUrl?.value || "", updateInterval: parseInt(this.elements.updateInterval?.value || 24), + + // Generic webhook + genericWebhook: { + enabled: this.elements.genericWebhookEnabled?.checked || false, + url: this.elements.genericWebhookUrl?.value || "", + events: [ + "detection_alert", + "false_positive_report", + "page_blocked", + "rogue_app_detected", + "threat_detected", + "validation_event" + ].filter(eventType => + document.getElementById(`webhookEvent_${eventType}`)?.checked + ) + }, // URL Allowlist settings urlAllowlist: this.elements.urlAllowlist?.value diff --git a/package.json b/package.json index 82c45908..b23c1109 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "doc": "docs" }, "scripts": { - "test": "node --test" + "test": "node --test tests/**/*.test.js", + "test:config": "node --test tests/config-persistence.test.js" }, "keywords": [], "author": "", diff --git a/popup/popup.css b/popup/popup.css index 4b935137..e35874c6 100644 --- a/popup/popup.css +++ b/popup/popup.css @@ -797,7 +797,7 @@ body { left: 0; width: 100%; height: 100%; - background: rgba(255, 255, 255, 0.9); + background: var(--theme-bg-primary); display: flex; flex-direction: column; align-items: center; @@ -822,7 +822,7 @@ body { .loading-text { font-size: 12px; - color: #6b7280; + color: var(--theme-text-primary); font-weight: 500; } diff --git a/rules/detection-rules.json b/rules/detection-rules.json index 3f364eaa..351b3ea3 100644 --- a/rules/detection-rules.json +++ b/rules/detection-rules.json @@ -3,54 +3,58 @@ "lastUpdated": "2025-09-08T14:20:00Z", "description": "Phishing detection logic for identifying phishing attempts targeting Microsoft 365 login pages", "trusted_login_patterns": [ - "^https://login\\.microsoftonline\\.(com|us)$", - "^https://login\\.microsoft\\.com$", - "^https://login\\.microsoft\\.net$", - "^https://login\\.windows\\.net$", - "^https://login\\.partner\\.microsoftonline\\.cn$", - "^https://login\\.live\\.com$" + "^https:\\/\\/login\\.microsoftonline\\.(com|us)$", + "^https:\\/\\/login\\.microsoft\\.com$", + "^https:\\/\\/login\\.microsoft\\.net$", + "^https:\\/\\/login\\.windows\\.net$", + "^https:\\/\\/login\\.partner\\.microsoftonline\\.cn$", + "^https:\\/\\/login\\.live\\.com$" ], "microsoft_domain_patterns": [ - "^https://[^.]*\\.microsoft\\.com$", - "^https://[^.]*\\.microsoftonline\\.com$", - "^https://[^.]*\\.office\\.com$", - "^https://[^.]*\\.office365\\.com$", - "^https://[^.]*\\.sharepoint\\.com$", - "^https://[^.]*\\.onedrive\\.com$", - "^https://[^.]*\\.live\\.com$", - "^https://[^.]*\\.hotmail\\.com$", - "^https://[^.]*\\.outlook\\.com$", - "^https://[^.]*\\.azure\\.com$", - "^https://[^.]*\\.azurewebsites\\.net$", - "^https://[^.]*\\.msauth\\.net$", - "^https://[^.]*\\.msftauth\\.net$", - "^https://[^.]*\\.msftauthimages\\.net$", - "^https://[^.]*\\.msauthimages\\.net$", - "^https://[^.]*\\.msidentity\\.com$", - "^https://[^.]*\\.microsoftonline-p\\.com$", - "^https://[^.]*\\.microsoftazuread-sso\\.com$", - "^https://[^.]*\\.azureedge\\.net$", - "^https://[^.]*\\.bing\\.com$", - "^https://.*\\.cloud\\.microsoft$", - "^https://([^.]+\\.)*live\\.com(/.*)?$" + "^https:\\/\\/[^.]*\\.microsoft\\.com$", + "^https:\\/\\/[^.]*\\.microsoftonline\\.com$", + "^https:\\/\\/[^.]*\\.office\\.com$", + "^https:\\/\\/[^.]*\\.office365\\.com$", + "^https:\\/\\/[^.]*\\.sharepoint\\.com$", + "^https:\\/\\/[^.]*\\.onedrive\\.com$", + "^https:\\/\\/[^.]*\\.live\\.com$", + "^https:\\/\\/[^.]*\\.hotmail\\.com$", + "^https:\\/\\/[^.]*\\.outlook\\.com$", + "^https:\\/\\/.*\\.azure\\.(com|cn|net)$", + "^https:\\/\\/[^.]*\\.azurewebsites\\.net$", + "^https:\\/\\/[^.]*\\.msauth\\.net$", + "^https:\\/\\/[^.]*\\.msftauth\\.net$", + "^https:\\/\\/[^.]*\\.msftauthimages\\.net$", + "^https:\\/\\/[^.]*\\.msauthimages\\.net$", + "^https:\\/\\/[^.]*\\.msidentity\\.com$", + "^https:\\/\\/[^.]*\\.microsoftonline-p\\.com$", + "^https:\\/\\/[^.]*\\.microsoftazuread-sso\\.com$", + "^https:\\/\\/[^.]*\\.azureedge\\.net$", + "^https:\\/\\/[^.]*\\.bing\\.com$", + "^https:\\/\\/github\\.com$", + "^https:\\/\\/.*\\.cloud\\.microsoft$", + "^https:\\/\\/([^.]+\\.)*live\\.com(/.*)?$" ], "exclusion_system": { "description": "Centralized exclusion system to prevent false positives on legitimate sites", "domain_patterns": [ - "^https://[^/]*\\.cipp\\.app(/.*)?$", - "^https://(.*\\.)?cyberdrain\\.com(/.*)?$", - "^https://[^/]*\\.cow\\.tech(/.*)?$", - "^https://[^/]*\\.auth0\\.com(/.*)?$", - "^https://[^/]*\\.google\\.(com|co\\.uk|ca|de|fr|co|nl|com\\.au)(/.*)?$", - "^https://[^/]*\\.bing\\.com(/.*)?$", - "^https://[^/]*\\.yahoo\\.com(/.*)?$", - "^https://[^/]*\\.duckduckgo\\.com(/.*)?$", - "^https://[^/]*\\.amazon\\.(com|co\\.uk|ca|de|fr)(/.*)?$", - "^https://[^/]*\\.(facebook|twitter|x|linkedin|instagram)\\.com(/.*)?$", - "^https://[^/]*\\.(youtube|youtu)\\.be(/.*)?$", - "^https://[^/]*\\.apple\\.com(/.*)?$", - "^https://[^/]*\\.dynamics\\.com(/.*)?$", - "^https://(?:[^/]*\\.)?zoom\\.us(/.*)?$" + "^https:\\/\\/[^/]*\\.cipp\\.app(/.*)?$", + "^https:\\/\\/(.*\\.)?cyberdrain\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.cow\\.tech(/.*)?$", + "^https:\\/\\/[^/]*\\.auth0\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.google\\.(com|co\\.uk|ca|de|fr|co|nl|com\\.au)(/.*)?$", + "^https:\\/\\/[^/]*\\.bing\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.yahoo\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.duckduckgo\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.amazon\\.(com|co\\.uk|ca|de|fr)(/.*)?$", + "^https:\\/\\/[^/]*\\.(facebook|twitter|x|linkedin|instagram)\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.(youtube|youtu)\\.be(/.*)?$", + "^https:\\/\\/[^/]*\\.apple\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.dynamics\\.com(/.*)?$", + "^https:\\/\\/(?:[^/]*\\.)?zoom\\.us(/.*)?$", + "^https:\\/\\/github\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.github\\.com(/.*)?$", + "^https:\\/\\/[^/]*\\.github\\.io(/.*)?$" ], "context_indicators": { "description": "Additional context that indicates legitimate discussion vs phishing", @@ -175,7 +179,7 @@ { "id": "password_input_field", "type": "source_content", - "pattern": "(?:type=[\"']password[\"']|name=[\"']password[\"']|id=[\"'].*password.*[\"'])", + "pattern": "]*(?:type=[\"']password[\"']|name=[\"']password[\"']|id=[\"'][^\"']*password[^\"']*[\"'])[^>]*>", "description": "Password input field present on page (supporting evidence only)", "weight": 1, "category": "secondary" @@ -183,8 +187,8 @@ { "id": "login_form_element", "type": "source_content", - "pattern": "(?:]*method=[\"']post[\"'][^>]*>|]*type=[\"'](?:email|text|tel)[\"'][^>]*placeholder=[\"'][^\"']+[\"'][^>]*>)", - "description": "Login form with POST method or a text/email/tel input with a placeholder", + "pattern": "]*type=[\"'](?:email|text|tel)[\"'][^>]*(?:placeholder=[\"'][^\"']+[\"'][^>]*|[^>]*)>", + "description": "Login form input field (email/text/tel type) with placeholder attribute", "weight": 1, "category": "secondary" } @@ -218,7 +222,7 @@ "description": "Block if customcss is loaded from non-Microsoft CDN", "condition": { "resource_pattern": "customcss", - "required_origin": "https://aadcdn.msftauthimages.net/", + "required_origin": "https:\\/\\/aadcdn.msftauthimages.net/", "block_if_different_origin": true }, "action": "block", @@ -261,19 +265,26 @@ "aad_detection_elements": [ { "id": "loginfmt_field", - "selectors": ["input[name='loginfmt']", "#i0116"], + "selectors": [ + "input[name='loginfmt']", + "#i0116" + ], "description": "Azure AD username/email input field", "weight": 30 }, { "id": "next_button", - "selectors": ["#idSIButton9"], + "selectors": [ + "#idSIButton9" + ], "description": "Azure AD Next/Sign in button", "weight": 25 }, { "id": "password_field", - "selectors": ["input[type='password']"], + "selectors": [ + "input[type='password']" + ], "description": "Password input field", "weight": 20 }, @@ -303,19 +314,25 @@ }, { "id": "urlMsaSignUp", - "text_patterns": ["urlMsaSignUp"], + "text_patterns": [ + "urlMsaSignUp" + ], "description": "Microsoft signup URL reference", "weight": 15 }, { "id": "flowToken", - "text_patterns": ["flowToken"], + "text_patterns": [ + "flowToken" + ], "description": "Microsoft authentication flow token", "weight": 15 }, { "id": "aadcdn_msauth", - "text_patterns": ["https://aadcdn\\.msauth\\.net/"], + "text_patterns": [ + "https:\\/\\/aadcdn\\.msauth\\.net/" + ], "description": "Microsoft authentication CDN reference", "weight": 15 } @@ -326,7 +343,9 @@ "type": "url", "weight": 25, "condition": { - "domains": ["login.microsoftonline.com"] + "domains": [ + "login.microsoftonline.com" + ] }, "description": "Verify legitimate Microsoft domain (must be login.microsoftonline.com)" }, @@ -368,7 +387,7 @@ "type": "content", "weight": 15, "condition": { - "contains": "https://aadcdn.msauth.net/", + "contains": "https:\\/\\/aadcdn.msauth.net/", "search_context": "page_source" }, "description": "Detect aadcdn.msauth.net presence in source" @@ -378,7 +397,10 @@ "type": "dom", "weight": 20, "condition": { - "selectors": ["input[name='loginfmt']", "#i0116"] + "selectors": [ + "input[name='loginfmt']", + "#i0116" + ] }, "description": "Check for loginfmt input field availability" }, @@ -398,34 +420,10 @@ "weight": 25, "condition": { "network_pattern": "*customcss*", - "required_domain": "https://aadcdn.msftauthimages.net/" + "required_domain": "https:\\/\\/aadcdn.msftauthimages.net/" }, "description": "Custom CSS files must originate from aadcdn.msftauthimages.net" }, - { - "id": "check_beginauth_csp", - "type": "header", - "weight": 30, - "condition": { - "header_name": "content-security-policy-report-only", - "required_domains": [ - "https://*.msauth.net/", - "https://*.msftauth.net/", - "https://*.msftauthimages.net/", - "https://*.msauthimages.net/", - "https://*.msidentity.com/", - "https://*.microsoftonline-p.com/", - "https://*.microsoftazuread-sso.com/", - "https://*.azureedge.net/", - "https://*.outlook.com/", - "https://*.office.com/", - "https://*.office365.com/", - "https://*.microsoft.com/", - "https://*.bing.com/" - ] - }, - "description": "BeginAuth request must contain proper content-security-policy-report-only header" - }, { "id": "check_valid_referrer", "type": "referrer_validation", @@ -496,7 +494,7 @@ }, { "id": "phi_006", - "pattern": "(?=.*(?:microsoft|office|365))(?=.*(?:login|password|signin))(?=.*form.*action)(?!.*login\\.microsoftonline\\.com)(?!.*\\.auth/login/)(?!.*azure\\s+static\\s+web\\s+apps)(?!.*easy\\s+auth)", + "pattern": "(?:microsoft|office|365).{0,2000}(?:login|password|signin).{0,500}form.{0,200}action(?!.*login\\.microsoftonline\\.com)(?!.*\\.auth/login/)(?!.*azure\\s+static\\s+web\\s+apps)(?!.*easy\\s+auth)", "flags": "i", "severity": "high", "description": "Microsoft-branded login form POST URL not pointing to login.microsoftonline.com", @@ -506,7 +504,7 @@ }, { "id": "phi_010_aad_fingerprint", - "pattern": "(?=.*(?:loginfmt|i0116))(?=.*(?:idSIButton9|type=[\"']submit[\"']))(?!.*login\\.microsoftonline\\.com)", + "pattern": "(?:loginfmt|i0116).{0,1000}(?:idSIButton9|type=[\"']submit[\"'])(?!.*login\\.microsoftonline\\.com)", "flags": "i", "severity": "critical", "description": "AAD-like login interface detected on non-Microsoft domain", @@ -516,7 +514,7 @@ }, { "id": "phi_011_missing_elements", - "pattern": "(?=.*(?:microsoft|office|365))(?=.*(?:type=[\"']password[\"']|method=[\"']post[\"']))(?!.*(?:idPartnerPL|urlMsaSignUp|flowToken))", + "pattern": "(?:microsoft|office|365).{0,2000}(?:type=[\"']password[\"']|method=[\"']post[\"'])(?!.*(?:idPartnerPL|urlMsaSignUp|flowToken))", "flags": "i", "severity": "high", "description": "Microsoft branding without required authentication elements", @@ -526,7 +524,7 @@ }, { "id": "phi_013_form_action_mismatch", - "pattern": "(?=.*(?:microsoft|office|365))(?=.*(?:password|passwd))(?=.*action=)(?!.*login\\.microsoftonline\\.com)", + "pattern": "(?:microsoft|office|365).{0,1500}(?:password|passwd).{0,500}action=(?!.*login\\.microsoftonline\\.com)", "flags": "i", "severity": "critical", "description": "Microsoft-branded password form with non-Microsoft action URL", @@ -536,7 +534,7 @@ }, { "id": "phi_014_devtools_blocking", - "pattern": "(?:debugger|developer.*tools?|devtools?|F12|inspect.*element|right.*click.*disabled|console.*clear|setInterval.*debugger|while.*true.*debugger|keydown.*F12|contextmenu.*prevent|selectstart.*prevent|dragstart.*prevent|syntaxerror019/HTML-STO|ld\\.min\\.js|lockdown\\.js|_0x[a-f0-9]+.*keyCode|function\\s*_0x[a-f0-9]+.*preventDefault|ctrlKey.*shiftKey.*keyCode.*0x[a-f0-9]+|addEventListener.*keydown.*0x[a-f0-9]+|contextmenu.*preventDefault.*mitigated)", + "pattern": "(?:debugger|devtools?|F12.*prevent|contextmenu.*prevent|selectstart.*prevent|setInterval.*debugger|while.*true.*debugger)", "flags": "i", "severity": "high", "description": "Page attempts to block or detect developer tools usage", @@ -571,7 +569,7 @@ }, { "id": "phi_015_code_obfuscation", - "pattern": "(?:eval\\s*\\(\\s*(?:atob|unescape|decodeURIComponent)|(?:new\\s+)?Function\\s*\\([^)]*atob|setInterval\\s*\\([^)]*(?:atob|eval)|setTimeout\\s*\\([^)]*(?:atob|eval)|document\\.write\\s*\\([^)]*(?:atob|unescape))", + "pattern": "(?:eval\\s*\\(\\s*(?:atob|unescape|decodeURIComponent)|(?:new\\s+)?Function\\s*\\([^)]{0,100}atob|setInterval\\s*\\([^)]{0,100}(?:atob|eval)|setTimeout\\s*\\([^)]{0,100}(?:atob|eval)|document\\.write\\s*\\([^)]{0,100}(?:atob|unescape))", "flags": "i", "severity": "high", "description": "Page contains suspicious JavaScript obfuscation patterns commonly used in malware", @@ -625,7 +623,7 @@ }, { "id": "phi_002", - "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth))(?:microsoft|office|365).*(?:security|verification|account).*(?:team|department|support)", + "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth))(?:microsoft|office|365).{0,500}(?:security|verification|account).{0,300}(?:team|department|support)", "flags": "i", "severity": "high", "description": "Impersonation of Microsoft security team (excludes legitimate SSO and third-party auth)", @@ -635,7 +633,7 @@ }, { "id": "phi_003", - "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|authenticate\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth))(?:verify.*account|suspended.*365|update.*office|secure.*microsoft|account.*security|security.*verification|365.*suspended|office.*update|microsoft.*secure|login.*microsoft|microsoft.*login|microsoft.*authentication|authentication.*microsoft|office.*365.*login|365.*office.*login)", + "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|authenticate\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth))(?:verify.{0,200}account|suspended.{0,200}365|update.{0,200}office|secure.{0,200}microsoft|account.{0,200}security|security.{0,200}verification|365.{0,200}suspended|office.{0,200}update|microsoft.{0,200}secure|login.{0,200}microsoft|microsoft.{0,200}login|microsoft.{0,200}authentication|authentication.{0,200}microsoft|office.{0,200}365.{0,200}login|365.{0,200}office.{0,200}login)", "flags": "i", "severity": "high", "description": "Common Microsoft 365 phishing keywords and variations", @@ -678,7 +676,7 @@ }, { "id": "phi_017_microsoft_brand_abuse", - "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|authenticate\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth|discussion|forum|community|tutorial|guide|documentation|support|help|wiki|blog|article|how\\s+to|step\\s+by\\s+step|configure|setup|administration|management))(?:(?:microsoft|office|365).*(?:login|sign.*in|authentication)|(?:login|sign.*in|authentication).*(?:microsoft|office|365))", + "pattern": "(?!.*(?:sign\\s+in\\s+with\\s+microsoft|continue\\s+with\\s+microsoft|login\\s+with\\s+microsoft|authenticate\\s+with\\s+microsoft|sso|oauth|third.?party\\s+auth|\\.auth/login/|azure\\s+static\\s+web\\s+apps|easy\\s+auth|discussion|forum|community|tutorial|guide|documentation|support|help|wiki|blog|article|how\\s+to|step\\s+by\\s+step|configure|setup|administration|management))(?:(?:microsoft|office|365).{0,1000}(?:login|sign.{0,50}in|authentication)|(?:login|sign.{0,50}in|authentication).{0,1000}(?:microsoft|office|365))", "flags": "i", "severity": "high", "description": "Microsoft branding combined with login/authentication terms on non-Microsoft domain", @@ -709,33 +707,35 @@ "content_patterns": [ "urlMsaSignUp", "flowToken", - "https://aadcdn.msauth.net/" + "https:\\/\\/aadcdn.msauth.net/" ], "description": "Legitimate Microsoft authentication code patterns", "confidence": 0.85 }, { "id": "leg_004", - "resource_patterns": ["https://aadcdn.msftauthimages.net/.*customcss.*"], + "resource_patterns": [ + "https:\\/\\/aadcdn.msftauthimages.net/.*customcss.*" + ], "description": "Legitimate source for custom CSS resources", "confidence": 0.9 }, { "id": "leg_005", "csp_domains": [ - "https://*.msauth.net/", - "https://*.msftauth.net/", - "https://*.msftauthimages.net/", - "https://*.msauthimages.net/", - "https://*.msidentity.com/", - "https://*.microsoftonline-p.com/", - "https://*.microsoftazuread-sso.com/", - "https://*.azureedge.net/", - "https://*.outlook.com/", - "https://*.office.com/", - "https://*.office365.com/", - "https://*.microsoft.com/", - "https://*.bing.com/" + "https:\\/\\/*.msauth.net/", + "https:\\/\\/*.msftauth.net/", + "https:\\/\\/*.msftauthimages.net/", + "https:\\/\\/*.msauthimages.net/", + "https:\\/\\/*.msidentity.com/", + "https:\\/\\/*.microsoftonline-p.com/", + "https:\\/\\/*.microsoftazuread-sso.com/", + "https:\\/\\/*.azureedge.net/", + "https:\\/\\/*.outlook.com/", + "https:\\/\\/*.office.com/", + "https:\\/\\/*.office365.com/", + "https:\\/\\/*.microsoft.com/", + "https:\\/\\/*.bing.com/" ], "description": "Required domains in content-security-policy-report-only header", "confidence": 1.0 @@ -743,13 +743,13 @@ { "id": "leg_006", "referrer_patterns": [ - "https://login\\.microsoftonline\\.com", - "https://login\\.microsoft\\.net", - "https://login\\.microsoft\\.com", - "https://autologon\\.microsoftazuread-sso\\.com", - "https://tasks\\.office\\.com", - "https://login\\.windows\\.net", - "https://planner\\.cloud\\.microsoft" + "https:\\/\\/login\\.microsoftonline\\.com", + "https:\\/\\/login\\.microsoft\\.net", + "https:\\/\\/login\\.microsoft\\.com", + "https:\\/\\/autologon\\.microsoftazuread-sso\\.com", + "https:\\/\\/tasks\\.office\\.com", + "https:\\/\\/login\\.windows\\.net", + "https:\\/\\/planner\\.cloud\\.microsoft" ], "description": "Valid Microsoft referrers from custom allow list", "confidence": 0.95 @@ -903,7 +903,9 @@ "action": "warn", "category": "code_obfuscation", "confidence": 0.7, - "context_required": ["(?:atob|eval|innerHTML|document\\.write)"] + "context_required": [ + "(?:atob|eval|innerHTML|document\\.write)" + ] }, { "id": "phi_022_cross_origin_fullscreen_iframe", @@ -938,7 +940,9 @@ { "id": "validate_css_origin", "pattern": "customcss", - "required_origins": ["aadcdn.msftauthimages.net"], + "required_origins": [ + "aadcdn.msftauthimages.net" + ], "action": "block", "description": "Custom CSS must come from Microsoft CDN" } @@ -956,4 +960,4 @@ "auto_update": true, "fallback_on_error": true } -} +} \ No newline at end of file diff --git a/scripts/background.js b/scripts/background.js index 3861dcd1..6155e136 100644 --- a/scripts/background.js +++ b/scripts/background.js @@ -7,6 +7,7 @@ import { ConfigManager } from "./modules/config-manager.js"; import { PolicyManager } from "./modules/policy-manager.js"; import { DetectionRulesManager } from "./modules/detection-rules-manager.js"; +import { WebhookManager } from "./modules/webhook-manager.js"; import logger from "./utils/logger.js"; import { store as storeLog } from "./utils/background-logger.js"; @@ -286,6 +287,7 @@ class CheckBackground { this.policyManager = new PolicyManager(); this.detectionRulesManager = new DetectionRulesManager(); this.rogueAppsManager = new RogueAppsManager(); + this.webhookManager = new WebhookManager(this.configManager); this.isInitialized = false; this.initializationPromise = null; this.initializationRetries = 0; @@ -1403,6 +1405,9 @@ class CheckBackground { // Update the configuration await this.configManager.updateConfig(message.config); + // Reload DetectionRulesManager configuration to pick up customRulesUrl changes + await this.detectionRulesManager.reloadConfiguration(); + // Get the updated config to check new badge setting const updatedConfig = await this.configManager.getConfig(); const newBadgeEnabled = @@ -1524,7 +1529,6 @@ class CheckBackground { return; } - // Handle CIPP report from content script await this.handleCippReport(message.payload); sendResponse({ success: true }); } catch (error) { @@ -1533,6 +1537,35 @@ class CheckBackground { } break; + case "send_webhook": + try { + if (!message.webhookType || !message.data) { + sendResponse({ success: false, error: "Invalid webhook message" }); + return; + } + + const userProfile = await this.getCurrentProfile(); + const config = await this.configManager.getConfig(); + + const metadata = { + config: config, + userProfile: userProfile, + extensionVersion: chrome.runtime.getManifest().version + }; + + const result = await this.webhookManager.sendWebhook( + message.webhookType, + message.data, + metadata + ); + + sendResponse({ success: result.success, result: result }); + } catch (error) { + logger.error("Check: Failed to send webhook:", error); + sendResponse({ success: false, error: error.message }); + } + break; + default: sendResponse({ success: false, error: "Unknown message type" }); } @@ -1569,6 +1602,8 @@ class CheckBackground { }); // CyberDrain integration - Refresh policy with defensive handling await this.refreshPolicy(); + // Reload DetectionRulesManager configuration to pick up policy changes + await safe(this.detectionRulesManager.reloadConfiguration()); } } @@ -2144,7 +2179,6 @@ class CheckBackground { // Handle CIPP reports from content script async handleCippReport(basePayload) { try { - // Get configuration const config = await this.configManager.getConfig(); if (!config?.enableCippReporting || !config?.cippServerUrl) { @@ -2152,111 +2186,29 @@ class CheckBackground { return; } - // Build the complete CIPP URL - const cippUrl = - config.cippServerUrl.replace(/\/+$/, "") + "/api/PublicPhishingCheck"; - - // Get user profile information const userProfile = await this.getCurrentProfile(); - // Extract user information from the profile structure - const userEmail = userProfile?.userInfo?.email || null; - const userDisplayName = - userProfile?.userInfo?.displayName || - userProfile?.userInfo?.name || - (userEmail ? userEmail.split("@")[0] : null); - - // Extract browser and environment context - const browserContext = { - browserType: userProfile?.browserInfo?.browserType || "unknown", - browserVersion: userProfile?.browserInfo?.browserVersion || "unknown", - platform: userProfile?.browserInfo?.platform || "unknown", - language: userProfile?.browserInfo?.language || "unknown", - extensionVersion: - userProfile?.browserInfo?.version || - chrome.runtime.getManifest().version, - installType: userProfile?.browserInfo?.installType || "unknown", - }; - - // Enhance payload with comprehensive security context - const enhancedPayload = { - // Original threat data - ...basePayload, - - // Tenant and user context - tenantId: config.cippTenantId || null, - userEmail: userEmail, - userDisplayName: userDisplayName, - accountType: userProfile?.userInfo?.accountType || "unknown", - isManaged: userProfile?.isManaged || false, - profileId: userProfile?.profileId || null, - - // Browser and environment context - browserContext: browserContext, - - // Security classification - alertSeverity: this.mapSeverityLevel( - basePayload.severity || basePayload.threatLevel - ), - alertCategory: this.categorizeSecurityEvent(basePayload), - - // Additional context for CIPP analytics - detectionMethod: "chrome_extension", - extensionId: chrome.runtime.id, - reportVersion: "2.0", // Version identifier for CIPP processing - - // Include redirect information if available (important for OAuth attacks) - ...(basePayload.redirectTo && { - redirectContext: { - redirectHost: basePayload.redirectTo, - isLocalhost: basePayload.redirectTo?.includes("localhost"), - isPrivateIP: this.isPrivateIP(basePayload.redirectTo), - }, - }), - - // Include client app information if available (for OAuth threats) - ...(basePayload.clientId && { - oauthContext: { - clientId: basePayload.clientId, - appName: basePayload.appName || "Unknown", - ...(basePayload.reason && { threatReason: basePayload.reason }), - }, - }), + const metadata = { + config: config, + userProfile: userProfile, + extensionVersion: chrome.runtime.getManifest().version, + isPrivateIP: this.webhookManager.isPrivateIP(basePayload.redirectTo) }; - logger.log(`Sending enhanced CIPP report to: ${cippUrl}`); - logger.debug( - `Report type: ${basePayload.type}, severity: ${enhancedPayload.alertSeverity}, category: ${enhancedPayload.alertCategory}` + const result = await this.webhookManager.sendWebhook( + this.webhookManager.webhookTypes.DETECTION_ALERT, + basePayload, + metadata ); - if (config.cippTenantId) { - logger.debug(`Including tenant ID: ${config.cippTenantId}`); - } - if (userEmail) { - logger.debug(`Including user profile: ${userEmail}`); + if (!result.success) { + throw new Error(result.error || "Failed to send webhook"); } - // Send POST request to CIPP server (no OPTIONS query needed) - const response = await fetch(cippUrl, { - method: "POST", - headers: { - "Content-Type": "application/json", - "User-Agent": `Check/${chrome.runtime.getManifest().version}`, - "X-Report-Version": "2.0", // Header to help CIPP identify enhanced reports - }, - body: JSON.stringify(enhancedPayload), - }); - - if (!response.ok) { - throw new Error( - `CIPP server responded with ${response.status}: ${response.statusText}` - ); - } - - logger.log("✅ Enhanced CIPP report sent successfully"); + logger.log("✅ Detection alert webhook sent successfully"); } catch (error) { - logger.error("Failed to send CIPP report:", error); - throw error; // Re-throw so content script gets the error + logger.error("Failed to send detection alert webhook:", error); + throw error; } } diff --git a/scripts/blocked.js b/scripts/blocked.js index 7d669952..a998c317 100644 --- a/scripts/blocked.js +++ b/scripts/blocked.js @@ -3,6 +3,10 @@ * Handles URL defanging, branding, and user interactions for blocked pages */ +// Store detection details globally for false positive reporting +let globalDetectionDetails = null; +let webhookConfig = null; + // Parse URL parameters to get block details with enhanced defanging function parseUrlParams() { console.log("parseUrlParams called"); @@ -23,6 +27,9 @@ function parseUrlParams() { try { const details = JSON.parse(decodeURIComponent(detailsParam)); console.log("Parsed details:", details); + + // Store details globally for false positive reporting + globalDetectionDetails = details; // Update blocked URL with defanging if (details.url) { @@ -121,6 +128,82 @@ function goBack() { } } +async function reportFalsePositive() { + console.log("reportFalsePositive function called"); + + const reportBtn = document.getElementById("reportFalsePositiveBtn"); + + if (!webhookConfig || !webhookConfig.url) { + console.error("No webhook configured"); + return; + } + + try { + reportBtn.disabled = true; + reportBtn.textContent = "Sending..."; + reportBtn.style.background = "#6b7280"; + reportBtn.style.color = "white"; + + const payload = { + version: "1.0", + type: "false_positive_report", + timestamp: new Date().toISOString(), + source: "Check Extension", + extensionVersion: chrome.runtime.getManifest().version, + data: { + reportedUrl: document.getElementById("blockedUrl").textContent, + reportedReason: document.getElementById("blockReason").textContent, + reportTimestamp: new Date().toISOString(), + userAgent: navigator.userAgent, + browserInfo: { + platform: navigator.platform, + language: navigator.language, + vendor: navigator.vendor, + cookiesEnabled: navigator.cookieEnabled, + onLine: navigator.onLine + }, + screenResolution: { + width: window.screen.width, + height: window.screen.height, + availWidth: window.screen.availWidth, + availHeight: window.screen.availHeight, + colorDepth: window.screen.colorDepth + }, + detectionDetails: globalDetectionDetails || {}, + userComments: null + } + }; + + console.log("Sending false positive report to:", webhookConfig.url); + console.log("Report payload:", payload); + + const response = await fetch(webhookConfig.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Webhook-Type": "false_positive_report", + "X-Webhook-Version": "1.0" + }, + body: JSON.stringify(payload) + }); + + if (response.ok) { + console.log("False positive report sent successfully"); + reportBtn.textContent = "Report Sent Successfully"; + reportBtn.style.background = "#16a34a"; + reportBtn.style.color = "white"; + } else { + console.warn("False positive report failed with HTTP status:", response.status, response.statusText); + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + } catch (error) { + console.error("Failed to send false positive report:", error); + reportBtn.textContent = `Failed: ${error.message}`; + reportBtn.style.background = "#dc2626"; + reportBtn.style.color = "white"; + } +} + function contactAdmin() { console.log("contactAdmin function called"); @@ -521,6 +604,25 @@ async function loadBranding() { contactBtn.style.display = "none"; } } + + // Check if false positive webhook is configured and show button accordingly + const falsePositiveBtn = document.getElementById("reportFalsePositiveBtn"); + const genericWebhook = storageResult.genericWebhook; + if (genericWebhook && genericWebhook.enabled && genericWebhook.url) { + const events = genericWebhook.events || []; + if (events.includes("false_positive_report")) { + console.log("False positive webhook configured, showing report button"); + webhookConfig = { url: genericWebhook.url }; + if (falsePositiveBtn) { + falsePositiveBtn.style.display = "inline-block"; + } + return; + } + } + console.log("No false positive webhook configured, hiding report button"); + if (falsePositiveBtn) { + falsePositiveBtn.style.display = "none"; + } return; // Exit early if we loaded from background script } @@ -585,6 +687,9 @@ document.addEventListener("DOMContentLoaded", () => { document .getElementById("contactAdminBtn") .addEventListener("click", contactAdmin); + document + .getElementById("reportFalsePositiveBtn") + .addEventListener("click", reportFalsePositive); // Add technical details toggle listener const techDetailsHeader = document.querySelector(".technical-details-header"); @@ -606,6 +711,9 @@ document.addEventListener("DOMContentLoaded", () => { document.getElementById("blockedUrl").textContent ); }, 1000); + + // Show the resulting page + document.body.classList.remove('loading'); }); // Handle keyboard shortcuts diff --git a/scripts/content.js b/scripts/content.js index 9d3a56c1..0dfeaea8 100644 --- a/scripts/content.js +++ b/scripts/content.js @@ -34,6 +34,13 @@ if (window.checkExtensionLoaded) { const WARNING_THRESHOLD = 3; // Block if 4+ warning threats found (escalation threshold) let initialBody; // Reference to the initial body element + const regexCache = new Map(); + let cachedPageSource = null; + let cachedPageSourceTime = 0; + const PAGE_SOURCE_CACHE_TTL = 1000; + const domQueryCache = new WeakMap(); + let cachedStylesheetAnalysis = null; + // Console log capturing let capturedLogs = []; const MAX_LOGS = 100; // Limit the number of stored logs @@ -112,6 +119,75 @@ if (window.checkExtensionLoaded) { }); } + function isInIframe() { + try { + return window.self !== window.top; + } catch (e) { + // If we can't access window.top due to cross-origin, we're likely in an iframe + return true; + } + } + + function getCachedRegex(pattern, flags = "") { + const key = `${pattern}|||${flags}`; + if (!regexCache.has(key)) { + try { + regexCache.set(key, new RegExp(pattern, flags)); + } catch (error) { + logger.warn(`Invalid regex pattern: ${pattern}`, error); + return null; + } + } + return regexCache.get(key); + } + + function getPageSource() { + const now = Date.now(); + if ( + !cachedPageSource || + now - cachedPageSourceTime > PAGE_SOURCE_CACHE_TTL + ) { + cachedPageSource = document.documentElement.outerHTML; + cachedPageSourceTime = now; + } + return cachedPageSource; + } + + function clearPerformanceCaches() { + cachedPageSource = null; + cachedPageSourceTime = 0; + domQueryCache.delete(document); + cachedStylesheetAnalysis = null; + } + + function analyzeStylesheets() { + if (cachedStylesheetAnalysis) return cachedStylesheetAnalysis; + const analysis = { hasMicrosoftCSS: false, cssContent: "", sheets: [] }; + try { + const styleSheets = Array.from(document.styleSheets); + for (const sheet of styleSheets) { + const sheetInfo = { href: sheet.href || "inline" }; + if (sheet.href?.match(/msauth|msft|microsoft/i)) { + analysis.hasMicrosoftCSS = true; + } + try { + if (sheet.cssRules) { + analysis.cssContent += + Array.from(sheet.cssRules) + .map((r) => r.cssText) + .join(" ") + " "; + sheetInfo.accessible = true; + } + } catch (e) { + sheetInfo.accessible = false; + } + analysis.sheets.push(sheetInfo); + } + } catch (e) {} + cachedStylesheetAnalysis = analysis; + return analysis; + } + /** * Check if a URL matches any pattern in the given pattern array * @param {string} url - The URL to check @@ -120,16 +196,11 @@ if (window.checkExtensionLoaded) { */ function matchesAnyPattern(url, patterns) { if (!patterns || patterns.length === 0) return false; - for (const pattern of patterns) { - try { - const regex = new RegExp(pattern); - if (regex.test(url)) { - logger.debug(`URL "${url}" matches pattern: ${pattern}`); - return true; - } - } catch (error) { - logger.warn(`Invalid regex pattern: ${pattern}`, error); + const regex = getCachedRegex(pattern); + if (regex && regex.test(url)) { + logger.debug(`URL "${url}" matches pattern: ${pattern}`); + return true; } } return false; @@ -202,6 +273,12 @@ if (window.checkExtensionLoaded) { developerConsoleLoggingEnabled = config.enableDeveloperConsoleLogging === true; // "Developer Mode" in UI + + // Only setup console capture if developer mode is enabled + if (developerConsoleLoggingEnabled) { + setupConsoleCapture(); + logger.log("Console capture enabled (developer mode active)"); + } } catch (error) { // If there's an error loading settings, default to false developerConsoleLoggingEnabled = false; @@ -222,6 +299,7 @@ if (window.checkExtensionLoaded) { domObserver.disconnect(); domObserver = null; } + clearPerformanceCaches(); setupDomObserver(); } @@ -327,7 +405,7 @@ if (window.checkExtensionLoaded) { */ function testDetectionPatterns() { console.log("🔍 MANUAL DETECTION TESTING"); - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); // Test each pattern individually const patterns = [ @@ -803,7 +881,7 @@ if (window.checkExtensionLoaded) { ); // Check for suspicious patterns in page source - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); const suspiciousPatterns = [ { name: "Microsoft mentions", @@ -861,7 +939,7 @@ if (window.checkExtensionLoaded) { } const requirements = detectionRules.m365_detection_requirements; - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); const pageText = document.body?.textContent || ""; // Lower threshold - just need ANY Microsoft-related elements @@ -980,7 +1058,7 @@ if (window.checkExtensionLoaded) { } const requirements = detectionRules.m365_detection_requirements; - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); // Store the page source for debugging purposes lastScannedPageSource = pageSource; @@ -1235,7 +1313,7 @@ if (window.checkExtensionLoaded) { case "css_spoofing_validation": // Check: If page has Microsoft CSS patterns but posts to non-Microsoft domain - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); let cssMatches = 0; // Count CSS indicator matches @@ -1642,7 +1720,7 @@ if (window.checkExtensionLoaded) { const threats = []; let totalScore = 0; - const pageSource = document.documentElement.outerHTML; + const pageSource = getPageSource(); const pageText = document.body?.textContent || ""; logger.log( @@ -1931,12 +2009,14 @@ if (window.checkExtensionLoaded) { * Now includes both detection rules exclusions AND user-configured URL allowlist */ function checkDomainExclusion(url) { + const urlObj = new URL(url); + const origin = urlObj.origin; if (detectionRules?.exclusion_system?.domain_patterns) { const rulesExcluded = detectionRules.exclusion_system.domain_patterns.some((pattern) => { try { const regex = new RegExp(pattern, "i"); - return regex.test(url); + return regex.test(origin); } catch (error) { logger.warn(`Invalid exclusion pattern: ${pattern}`); return false; @@ -1944,11 +2024,11 @@ if (window.checkExtensionLoaded) { }); if (rulesExcluded) { - logger.log(`✅ URL excluded by detection rules: ${url}`); + logger.log(`✅ URL excluded by detection rules: ${origin}`); return true; } } - return checkUserUrlAllowlist(url); + return checkUserUrlAllowlist(origin); } /** @@ -2074,7 +2154,7 @@ if (window.checkExtensionLoaded) { let score = 0; const triggeredRules = []; - const pageHTML = document.documentElement.outerHTML; + const pageHTML = getPageSource(); // Process each rule from the detection rules file for (const rule of detectionRules.rules) { @@ -2337,7 +2417,9 @@ if (window.checkExtensionLoaded) { async function runProtection(isRerun = false) { try { logger.log( - `🚀 Starting protection analysis ${isRerun ? "(re-run)" : "(initial)"}` + `🚀 Starting protection analysis ${ + isRerun ? "(re-run)" : "(initial)" + } for ${window.location.href}` ); logger.log( `📄 Page info: ${document.querySelectorAll("*").length} elements, ${ @@ -2345,6 +2427,10 @@ if (window.checkExtensionLoaded) { } chars content` ); + if (isInIframe()) { + logger.log("âš ī¸ Page is in an iframe"); + } + // Load configuration to check protection settings and URL allowlist const config = await new Promise((resolve) => { chrome.storage.local.get(["config"], (result) => { @@ -2574,11 +2660,31 @@ if (window.checkExtensionLoaded) { redirectTo: redirectHostname, }); - return; // Stop processing - do NOT show valid badge for rogue apps + // Send rogue_app_detected webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "rogue_app_detected", + data: { + url: location.href, + clientId: clientInfo.clientId, + appName: clientInfo.appInfo?.appName || "Unknown", + reason: clientInfo.reason, + severity: "critical", + risk: "high", + description: clientInfo.appInfo?.description, + tags: clientInfo.appInfo?.tags || [], + references: clientInfo.appInfo?.references || [], + redirectTo: redirectHostname + } + }).catch(err => { + logger.warn("Failed to send rogue_app_detected webhook:", err.message); + }); + + return; } // Only show valid badge if no rogue app detected - if (protectionEnabled) { + if (protectionEnabled && !isInIframe()) { // Ask background script to show valid badge (it will check if the setting is enabled) chrome.runtime.sendMessage( { type: "REQUEST_SHOW_VALID_BADGE" }, @@ -3091,6 +3197,26 @@ if (window.checkExtensionLoaded) { redirectTo: redirectHostname, }); + // Send rogue_app_detected webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "rogue_app_detected", + data: { + url: location.href, + clientId: clientInfo.clientId, + appName: clientInfo.appInfo?.appName || "Unknown", + reason: clientInfo.reason, + severity: "critical", + risk: "high", + description: clientInfo.appInfo?.description, + tags: clientInfo.appInfo?.tags || [], + references: clientInfo.appInfo?.references || [], + redirectTo: redirectHostname + } + }).catch(err => { + logger.warn("Failed to send rogue_app_detected webhook:", err.message); + }); + // Store detection result as critical threat lastDetectionResult = { verdict: "rogue-app", @@ -3144,11 +3270,29 @@ if (window.checkExtensionLoaded) { logger.error( "đŸ›Ąī¸ PROTECTION ACTIVE: Blocking page - redirecting to blocking page" ); - // Redirect to actual blocking page when protection is enabled + + // Send page_blocked webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "page_blocked", + data: { + url: location.href, + reason: blockingResult.reason, + severity: blockingResult.severity || "critical", + score: 0, + threshold: blockingResult.threshold || 85, + rule: blockingResult.rule?.id || "blocking_rule", + ruleDescription: blockingResult.reason, + timestamp: new Date().toISOString() + } + }).catch(err => { + logger.warn("Failed to send page_blocked webhook:", err.message); + }); + showBlockingOverlay(blockingResult.reason, blockingResult); disableFormSubmissions(); disableCredentialInputs(); - stopDOMMonitoring(); // Stop monitoring once we've blocked + stopDOMMonitoring(); } else { logger.warn( "âš ī¸ PROTECTION DISABLED: Would block but showing warning banner instead" @@ -3242,6 +3386,25 @@ if (window.checkExtensionLoaded) { logger.error( "đŸ›Ąī¸ PROTECTION ACTIVE: Blocking due to critical detection rule" ); + + // Send page_blocked webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "page_blocked", + data: { + url: location.href, + reason: reason, + severity: "critical", + score: 0, + threshold: detectionResult.threshold, + rule: criticalBlockingRules[0]?.id || "critical_rule", + ruleDescription: reason, + timestamp: new Date().toISOString() + } + }).catch(err => { + logger.warn("Failed to send page_blocked webhook:", err.message); + }); + showBlockingOverlay(reason, { threats: criticalBlockingRules.map((rule) => ({ description: rule.description, @@ -3361,6 +3524,25 @@ if (window.checkExtensionLoaded) { logger.error( "đŸ›Ąī¸ PROTECTION ACTIVE: Blocking page due to critical phishing indicators" ); + + // Send page_blocked webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "page_blocked", + data: { + url: location.href, + reason: reason, + severity: "critical", + score: 0, + threshold: detectionResult.threshold, + rule: criticalThreats[0]?.id || "critical_phishing", + ruleDescription: reason, + timestamp: new Date().toISOString() + } + }).catch(err => { + logger.warn("Failed to send page_blocked webhook:", err.message); + }); + showBlockingOverlay(reason, { threats: criticalThreats, score: phishingResult.score, @@ -3532,6 +3714,25 @@ if (window.checkExtensionLoaded) { logger.error( "đŸ›Ąī¸ PROTECTION ACTIVE: Blocking page due to high threat" ); + + // Send page_blocked webhook + chrome.runtime.sendMessage({ + type: "send_webhook", + webhookType: "page_blocked", + data: { + url: location.href, + reason: reason, + severity: severity, + score: detectionResult.score, + threshold: detectionResult.threshold, + rule: detectionResult.triggeredRules?.[0] || "unknown", + ruleDescription: detectionResult.triggeredRules?.[0] || reason, + timestamp: new Date().toISOString() + } + }).catch(err => { + logger.warn("Failed to send page_blocked webhook:", err.message); + }); + showBlockingOverlay(reason, lastDetectionResult); disableFormSubmissions(); disableCredentialInputs(); @@ -4083,26 +4284,45 @@ if (window.checkExtensionLoaded) { showingBanner = true; // Fetch branding configuration (uniform pattern: storage only, like applyBrandingColors) - const fetchBranding = () => new Promise((resolve) => { - try { - chrome.storage.local.get(["brandingConfig"], (result) => { - resolve(result?.brandingConfig || {}); - }); - } catch(_) { resolve({}); } - }); + const fetchBranding = () => + new Promise((resolve) => { + try { + chrome.storage.local.get(["brandingConfig"], (result) => { + resolve(result?.brandingConfig || {}); + }); + } catch (_) { + resolve({}); + } + }); const extractPhishingIndicators = (details) => { if (!details) return "Unknown detection criteria"; // Try to extract phishing indicators from various possible fields // This matches the exact logic from blocked.js openMailto function - if (details.phishingIndicators && Array.isArray(details.phishingIndicators)) { + if ( + details.phishingIndicators && + Array.isArray(details.phishingIndicators) + ) { return details.phishingIndicators - .map(indicator => `- ${indicator.id || indicator.name || "Unknown"}: ${indicator.description || indicator.reason || "Detected"}`) + .map( + (indicator) => + `- ${indicator.id || indicator.name || "Unknown"}: ${ + indicator.description || indicator.reason || "Detected" + }` + ) .join("\n"); - } else if (details.matchedRules && Array.isArray(details.matchedRules)) { + } else if ( + details.matchedRules && + Array.isArray(details.matchedRules) + ) { return details.matchedRules - .map(rule => `- ${rule.id || rule.name || "Unknown"}: ${rule.description || rule.reason || "Rule matched"}`) + .map( + (rule) => + `- ${rule.id || rule.name || "Unknown"}: ${ + rule.description || rule.reason || "Rule matched" + }` + ) .join("\n"); } else if (details.threats && Array.isArray(details.threats)) { // Filter out the summary threat and show only specific indicators @@ -4113,33 +4333,68 @@ if (window.checkExtensionLoaded) { return true; } // Keep threats with specific types that aren't summary types - if (threat.type && !threat.type.includes("threat") && threat.description) { + if ( + threat.type && + !threat.type.includes("threat") && + threat.description + ) { return true; } // Keep anything else that looks like a specific threat - return (threat.description && threat.description.length > 10 && threat.id); + return ( + threat.description && threat.description.length > 10 && threat.id + ); }); return specificThreats - .map(threat => `- ${threat.type || threat.category || threat.id || "Phishing Indicator"}: ${threat.description || threat.reason || "Threat detected"}`) + .map( + (threat) => + `- ${ + threat.type || + threat.category || + threat.id || + "Phishing Indicator" + }: ${threat.description || threat.reason || "Threat detected"}` + ) .join("\n"); - } else if (details.foundThreats && Array.isArray(details.foundThreats)) { + } else if ( + details.foundThreats && + Array.isArray(details.foundThreats) + ) { return details.foundThreats - .map(threat => `- ${threat.id || threat}: ${threat.description || "Detected"}`) + .map( + (threat) => + `- ${threat.id || threat}: ${threat.description || "Detected"}` + ) .join("\n"); } else if (details.indicators && Array.isArray(details.indicators)) { return details.indicators - .map(indicator => `- ${indicator.id}: ${indicator.description || indicator.id} (${indicator.severity || "unknown"})`) + .map( + (indicator) => + `- ${indicator.id}: ${indicator.description || indicator.id} (${ + indicator.severity || "unknown" + })` + ) .join("\n"); - } else if (details.foundIndicators && Array.isArray(details.foundIndicators)) { + } else if ( + details.foundIndicators && + Array.isArray(details.foundIndicators) + ) { return details.foundIndicators - .map(indicator => `- ${indicator.id || indicator}: ${indicator.description || ""}`) + .map( + (indicator) => + `- ${indicator.id || indicator}: ${indicator.description || ""}` + ) .join("\n"); } else { // Fallback: Look for any array properties that might contain indicators - const arrayProps = Object.keys(details).filter(key => Array.isArray(details[key]) && details[key].length > 0); - + const arrayProps = Object.keys(details).filter( + (key) => Array.isArray(details[key]) && details[key].length > 0 + ); + if (arrayProps.length > 0) { - return `Multiple indicators detected (${details.reason || "see browser console for details"})`; + return `Multiple indicators detected (${ + details.reason || "see browser console for details" + })`; } else { return `${details.reason || "Unknown detection criteria"}`; } @@ -4149,60 +4404,92 @@ if (window.checkExtensionLoaded) { const applyBranding = (bannerEl, branding) => { if (!bannerEl) return; try { - const companyName = branding.companyName || branding.productName || "CyberDrain"; + const companyName = + branding.companyName || branding.productName || "CyberDrain"; const supportEmail = branding.supportEmail || ""; let logoUrl = branding.logoUrl || ""; - const packagedFallback = chrome.runtime.getURL('images/icon48.png'); + const packagedFallback = chrome.runtime.getURL("images/icon48.png"); // Simplified: rely on upstream input validation; only fallback when empty/falsy if (!logoUrl) { logoUrl = packagedFallback; } - let brandingSlot = bannerEl.querySelector('#check-banner-branding'); + let brandingSlot = bannerEl.querySelector("#check-banner-branding"); if (!brandingSlot) { - const container = document.createElement('div'); - container.id = 'check-banner-branding'; - container.style.cssText = 'display:flex;align-items:center;gap:8px;'; + const container = document.createElement("div"); + container.id = "check-banner-branding"; + container.style.cssText = + "display:flex;align-items:center;gap:8px;"; const innerWrapper = bannerEl.firstElementChild; - if (innerWrapper) innerWrapper.insertBefore(container, innerWrapper.firstChild); + if (innerWrapper) + innerWrapper.insertBefore(container, innerWrapper.firstChild); brandingSlot = container; } if (brandingSlot) { - brandingSlot.innerHTML = ''; + brandingSlot.innerHTML = ""; if (logoUrl) { - const img = document.createElement('img'); + const img = document.createElement("img"); img.src = logoUrl; - img.alt = companyName + ' logo'; - img.style.cssText = 'width:28px;height:28px;object-fit:contain;border-radius:4px;background:rgba(255,255,255,0.25);padding:2px;'; + img.alt = companyName + " logo"; + img.style.cssText = + "width:28px;height:28px;object-fit:contain;border-radius:4px;background:rgba(255,255,255,0.25);padding:2px;"; brandingSlot.appendChild(img); } - const textWrap = document.createElement('div'); - textWrap.style.cssText = 'display:flex;flex-direction:column;align-items:flex-start;line-height:1.2;'; - const titleSpan = document.createElement('span'); - titleSpan.style.cssText = 'font-size:12px;font-weight:600;'; - titleSpan.textContent = 'Protected by ' + companyName; + const textWrap = document.createElement("div"); + textWrap.style.cssText = + "display:flex;flex-direction:column;align-items:flex-start;line-height:1.2;"; + const titleSpan = document.createElement("span"); + titleSpan.style.cssText = "font-size:12px;font-weight:600;"; + titleSpan.textContent = "Protected by " + companyName; textWrap.appendChild(titleSpan); if (supportEmail) { - const contactDiv = document.createElement('div'); - const contactLink = document.createElement('a'); - contactLink.style.cssText = 'color:#fff;text-decoration:underline;font-size:11px;cursor:pointer;'; - contactLink.textContent = 'Report as clean/safe'; - contactLink.title = 'Report this page as clean/safe to your administrator'; - contactLink.href = `mailto:${supportEmail}?subject=${encodeURIComponent('Security Review: Possible Clean/Safe Page')}`; - contactLink.addEventListener('click', (e) => { - try { chrome.runtime.sendMessage({ type: 'REPORT_FALSE_POSITIVE', url: location.href, reason }); } catch(_) {} + const contactDiv = document.createElement("div"); + const contactLink = document.createElement("a"); + contactLink.style.cssText = + "color:#fff;text-decoration:underline;font-size:11px;cursor:pointer;"; + contactLink.textContent = "Report as clean/safe"; + contactLink.title = + "Report this page as clean/safe to your administrator"; + contactLink.href = `mailto:${supportEmail}?subject=${encodeURIComponent( + "Security Review: Possible Clean/Safe Page" + )}`; + contactLink.addEventListener("click", (e) => { + try { + chrome.runtime.sendMessage({ + type: "REPORT_FALSE_POSITIVE", + url: location.href, + reason, + }); + } catch (_) {} let indicatorsText; - try { indicatorsText = extractPhishingIndicators(analysisData); } catch(err) { indicatorsText = 'Parse error - see console'; } - const detectionScoreLine = analysisData?.score !== undefined ? `Detection Score: ${analysisData.score}/${analysisData.threshold}` : 'Detection Score: N/A'; + try { + indicatorsText = extractPhishingIndicators(analysisData); + } catch (err) { + indicatorsText = "Parse error - see console"; + } + const detectionScoreLine = + analysisData?.score !== undefined + ? `Detection Score: ${analysisData.score}/${analysisData.threshold}` + : "Detection Score: N/A"; const subject = `Security Review: Mark Clean - ${location.hostname}`; - const body = encodeURIComponent(`Security Review Request: Possible Clean/Safe Page\n\nPage URL: ${location.href}\nHostname: ${location.hostname}\nTimestamp (UTC): ${new Date().toISOString()}\nBanner Title: ${bannerTitle}\nDisplayed Reason: ${reason}\n${detectionScoreLine}\n\nDetected Indicators:\n${indicatorsText}\n\nUser Justification:\n[Explain why this page is safe]`); - e.currentTarget.href = `mailto:${supportEmail}?subject=${encodeURIComponent(subject)}&body=${body}`; + const body = encodeURIComponent( + `Security Review Request: Possible Clean/Safe Page\n\nPage URL: ${ + location.href + }\nHostname: ${ + location.hostname + }\nTimestamp (UTC): ${new Date().toISOString()}\nBanner Title: ${bannerTitle}\nDisplayed Reason: ${reason}\n${detectionScoreLine}\n\nDetected Indicators:\n${indicatorsText}\n\nUser Justification:\n[Explain why this page is safe]` + ); + e.currentTarget.href = `mailto:${supportEmail}?subject=${encodeURIComponent( + subject + )}&body=${body}`; }); contactDiv.appendChild(contactLink); textWrap.appendChild(contactDiv); } brandingSlot.appendChild(textWrap); } - } catch(e) { /* non-fatal */ } + } catch (e) { + /* non-fatal */ + } }; const detailsText = analysisData?.score @@ -4262,7 +4549,7 @@ if (window.checkExtensionLoaded) { // Update existing banner content and color banner.innerHTML = bannerContent; banner.style.background = bannerColor; - fetchBranding().then(branding => applyBranding(banner, branding)); + fetchBranding().then((branding) => applyBranding(banner, branding)); // Ensure page content is still pushed down const bannerHeight = banner.offsetHeight || 64; @@ -4292,7 +4579,7 @@ if (window.checkExtensionLoaded) { banner.innerHTML = bannerContent; document.body.appendChild(banner); - fetchBranding().then(branding => applyBranding(banner, branding)); + fetchBranding().then((branding) => applyBranding(banner, branding)); // Push page content down to avoid covering login header const bannerHeight = banner.offsetHeight || 64; // fallback height @@ -4740,8 +5027,8 @@ if (window.checkExtensionLoaded) { try { logger.log("Initializing Check"); - // Setup console capture early to catch all logs - setupConsoleCapture(); + // Console capture is now setup only when developer mode is enabled (see loadDeveloperConsoleLoggingSetting) + // This eliminates performance overhead for normal users // Apply branding colors first applyBrandingColors(); @@ -4937,10 +5224,19 @@ if (window.checkExtensionLoaded) { if (message.type === "GET_CONSOLE_LOGS") { try { - sendResponse({ - success: true, - logs: capturedLogs.slice(), // Send a copy of the logs - }); + // Console logs only available if developer mode is enabled + if (!developerConsoleLoggingEnabled) { + sendResponse({ + success: false, + error: + "Console capture disabled. Enable Developer Mode in options to capture logs.", + }); + } else { + sendResponse({ + success: true, + logs: capturedLogs.slice(), // Send a copy of the logs + }); + } } catch (error) { sendResponse({ success: false, error: error.message }); } diff --git a/scripts/modules/config-manager.js b/scripts/modules/config-manager.js index 57703662..f1d1a910 100644 --- a/scripts/modules/config-manager.js +++ b/scripts/modules/config-manager.js @@ -29,6 +29,11 @@ export class ConfigManager { // Load local configuration with safe wrapper const localConfig = await safe(chrome.storage.local.get(["config"])); + // Migrate legacy configuration structure if needed + if (localConfig?.config) { + localConfig.config = this.migrateLegacyConfig(localConfig.config); + } + // Load branding configuration this.brandingConfig = await this.loadBrandingConfig(); @@ -47,6 +52,24 @@ export class ConfigManager { } } + migrateLegacyConfig(config) { + // Migrate legacy detectionRules.customRulesUrl to top-level customRulesUrl + if (config.detectionRules?.customRulesUrl && !config.customRulesUrl) { + config.customRulesUrl = config.detectionRules.customRulesUrl; + logger.log("Check: Migrated legacy customRulesUrl to top-level"); + } + + // Migrate legacy detectionRules.updateInterval to top-level updateInterval + if (config.detectionRules?.updateInterval && !config.updateInterval) { + // Convert milliseconds to hours if needed + const interval = config.detectionRules.updateInterval; + config.updateInterval = interval > 1000 ? Math.round(interval / 3600000) : interval; + logger.log("Check: Migrated legacy updateInterval to top-level"); + } + + return config; + } + async loadEnterpriseConfig() { try { // Safe wrapper for chrome.* operations @@ -194,6 +217,18 @@ export class ConfigManager { ...enterpriseConfig, }; + // Fix customRulesUrl precedence - user-saved value should override defaults but NOT enterprise + if (!enterpriseConfig?.customRulesUrl) { + if (localConfig?.customRulesUrl && localConfig.customRulesUrl.trim() !== "") { + merged.customRulesUrl = localConfig.customRulesUrl; + if (merged.detectionRules) { + merged.detectionRules.customRulesUrl = localConfig.customRulesUrl; + } + } else if (localConfig?.detectionRules?.customRulesUrl && localConfig.detectionRules.customRulesUrl.trim() !== "") { + merged.customRulesUrl = localConfig.detectionRules.customRulesUrl; + } + } + // Remove customBranding from the top level since it's been merged into branding if (merged.customBranding) { delete merged.customBranding; @@ -229,8 +264,6 @@ export class ConfigManager { // Detection settings detectionRules: { enableCustomRules: true, - customRulesUrl: - "https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/rules/detection-rules.json", updateInterval: 86400000, // 24 hours strictMode: false, }, @@ -244,8 +277,8 @@ export class ConfigManager { // Debug settings enableDebugLogging: false, - // Custom rules - customRulesUrl: "", + // Custom rules - centralized at top level + customRulesUrl: "https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/rules/detection-rules.json", updateInterval: 24, // hours // Performance settings @@ -343,8 +376,17 @@ export class ConfigManager { } }; - const currentConfig = await this.getConfig(); - const updatedConfig = { ...currentConfig, ...updates }; + // Get the CURRENT LOCAL CONFIG (not merged), so we only save user overrides + const localConfigResult = await safe(chrome.storage.local.get(["config"])); + const localConfig = localConfigResult?.config || {}; + + // Merge updates into the local config (not the merged config) + const updatedLocalConfig = { ...localConfig, ...updates }; + + // Remove empty customRulesUrl to allow fallback to default + if (updates.customRulesUrl !== undefined && updates.customRulesUrl.trim() === '') { + delete updatedLocalConfig.customRulesUrl; + } // Validate that enterprise-enforced policies are not being modified if (this.enterpriseConfig?.enforcedPolicies) { @@ -363,15 +405,18 @@ export class ConfigManager { ); } - await safe(chrome.storage.local.set({ config: updatedConfig })); - this.config = updatedConfig; + // Save only the user's config overrides to local storage + await safe(chrome.storage.local.set({ config: updatedLocalConfig })); + + // Reload the full merged config + await this.loadConfig(); // Notify other components of configuration change with safe wrapper try { chrome.runtime.sendMessage( { type: "CONFIG_UPDATED", - config: updatedConfig, + config: this.config, }, () => { if (chrome.runtime.lastError) { @@ -383,7 +428,7 @@ export class ConfigManager { // Silently handle errors } - return updatedConfig; + return this.config; } catch (error) { logger.error("Check: Failed to update configuration:", error); throw error; @@ -422,6 +467,12 @@ export class ConfigManager { logger.log("Check: Applied enterprise custom branding"); } + // Include genericWebhook from config if available + const currentConfig = await this.getConfig(); + if (currentConfig.genericWebhook) { + finalBranding.genericWebhook = currentConfig.genericWebhook; + } + return finalBranding; } diff --git a/scripts/modules/detection-rules-manager.js b/scripts/modules/detection-rules-manager.js index 25f804c2..b3dcdd12 100644 --- a/scripts/modules/detection-rules-manager.js +++ b/scripts/modules/detection-rules-manager.js @@ -82,6 +82,11 @@ export class DetectionRulesManager { } } + async reloadConfiguration() { + logger.log("DetectionRulesManager: Reloading configuration"); + await this.loadConfiguration(); + } + async loadFromCache() { try { const result = await chrome.storage.local.get([this.cacheKey]); @@ -231,6 +236,7 @@ export class DetectionRulesManager { async forceUpdate() { logger.log("Forcing detection rules update"); + await this.reloadConfiguration(); return await this.updateDetectionRules(); } diff --git a/scripts/modules/webhook-manager.js b/scripts/modules/webhook-manager.js new file mode 100644 index 00000000..de1dca7d --- /dev/null +++ b/scripts/modules/webhook-manager.js @@ -0,0 +1,372 @@ +import logger from "../utils/logger.js"; + +export class WebhookManager { + constructor(configManager) { + this.configManager = configManager; + this.webhookTypes = { + DETECTION_ALERT: "detection_alert", + FALSE_POSITIVE: "false_positive_report", + PAGE_BLOCKED: "page_blocked", + ROGUE_APP: "rogue_app_detected", + THREAT_DETECTED: "threat_detected", + VALIDATION_EVENT: "validation_event" + }; + } + + async getWebhookConfig(webhookType) { + const config = await this.configManager.getConfig(); + + if (!config) { + return null; + } + + if (webhookType === this.webhookTypes.DETECTION_ALERT && config.enableCippReporting) { + return { + url: config.cippServerUrl ? + config.cippServerUrl.replace(/\/+$/, "") + "/api/PublicPhishingCheck" : null, + enabled: config.enableCippReporting, + type: "cipp", + tenantId: config.cippTenantId || null + }; + } + + const genericWebhook = config.genericWebhook; + if (genericWebhook && genericWebhook.enabled && genericWebhook.url) { + const events = genericWebhook.events || []; + if (events.includes(webhookType)) { + return { + url: genericWebhook.url, + enabled: true, + type: "generic" + }; + } + } + + return null; + } + + buildPayload(webhookType, data, metadata = {}) { + const basePayload = { + version: "1.0", + type: webhookType, + timestamp: new Date().toISOString(), + source: "Check Extension", + extensionVersion: metadata.extensionVersion || chrome.runtime.getManifest().version, + data: {} + }; + + switch (webhookType) { + case this.webhookTypes.DETECTION_ALERT: + basePayload.data = this.buildDetectionAlertPayload(data); + break; + case this.webhookTypes.FALSE_POSITIVE: + basePayload.data = this.buildFalsePositivePayload(data); + break; + case this.webhookTypes.PAGE_BLOCKED: + basePayload.data = this.buildPageBlockedPayload(data); + break; + case this.webhookTypes.ROGUE_APP: + basePayload.data = this.buildRogueAppPayload(data); + break; + case this.webhookTypes.THREAT_DETECTED: + basePayload.data = this.buildThreatDetectedPayload(data); + break; + case this.webhookTypes.VALIDATION_EVENT: + basePayload.data = this.buildValidationEventPayload(data); + break; + default: + basePayload.data = data; + } + + if (metadata.userProfile) { + basePayload.user = this.sanitizeUserProfile(metadata.userProfile); + } + + if (metadata.browserContext) { + basePayload.browser = metadata.browserContext; + } + + if (metadata.tenantId) { + basePayload.tenantId = metadata.tenantId; + } + + return basePayload; + } + + buildDetectionAlertPayload(data) { + return { + url: data.url || data.targetUrl, + severity: data.severity || data.threatLevel || "medium", + score: data.score || data.threatScore || 0, + threshold: data.threshold || 85, + reason: data.reason || data.blockReason || "Threat detected", + detectionMethod: data.detectionMethod || "rules_engine", + rule: data.rule || data.ruleId || null, + ruleDescription: data.ruleDescription || data.reason || null, + category: data.category || "phishing", + confidence: data.confidence || 0.8, + matchedRules: data.rules || data.matchedRules || [], + context: { + referrer: data.referrer || null, + pageTitle: data.pageTitle || null, + domain: data.domain || null, + redirectTo: data.redirectTo || null + } + }; + } + + buildFalsePositivePayload(data) { + return { + url: data.blockedUrl || data.url, + severity: "info", + reason: data.blockReason || data.reason || "User reported false positive", + reportTimestamp: data.timestamp || new Date().toISOString(), + userAgent: data.userAgent || null, + browserInfo: data.browserInfo || {}, + screenResolution: data.screenResolution || {}, + detectionDetails: data.detectionDetails || {}, + userComments: data.comments || null, + context: { + referrer: null, + pageTitle: null, + domain: null + } + }; + } + + buildPageBlockedPayload(data) { + return { + url: data.url || data.blockedUrl, + severity: data.severity || data.threatLevel || "high", + score: data.score || 0, + threshold: data.threshold || 85, + reason: data.reason || data.blockReason || "Page blocked", + detectionMethod: data.detectionMethod || "rules_engine", + rule: data.rule || data.ruleId || null, + ruleDescription: data.ruleDescription || data.reason || null, + category: data.category || "phishing", + action: "blocked", + context: { + referrer: data.referrer || null, + pageTitle: data.pageTitle || null, + domain: data.domain || null, + redirectTo: data.redirectTo || null + } + }; + } + + buildRogueAppPayload(data) { + return { + url: data.url, + severity: data.severity || data.risk || "critical", + reason: data.reason || "Rogue OAuth application detected", + detectionMethod: "rogue_app_detection", + category: "oauth_threat", + clientId: data.clientId, + appName: data.appName || "Unknown", + appInfo: { + description: data.description || null, + tags: data.tags || [], + references: data.references || [], + risk: data.risk || "high" + }, + context: { + referrer: null, + pageTitle: null, + domain: null, + redirectTo: data.redirectTo || null, + isLocalhost: data.redirectTo?.includes("localhost") || false, + isPrivateIP: data.isPrivateIP || false + } + }; + } + + buildThreatDetectedPayload(data) { + return { + url: data.url, + severity: data.severity || "medium", + score: data.score || 0, + threshold: data.threshold || 85, + reason: data.reason || "Threat detected", + detectionMethod: data.detectionMethod || "content_analysis", + rule: data.rule || null, + category: data.category || data.type || data.threatType || "threat", + confidence: data.confidence || 0.7, + indicators: data.indicators || [], + matchedRules: data.matchedRules || [], + context: { + referrer: data.referrer || null, + pageTitle: data.pageTitle || null, + domain: data.domain || null, + redirectTo: data.redirectTo || null, + ...(data.context || {}) + } + }; + } + + buildValidationEventPayload(data) { + return { + url: data.url, + severity: "info", + reason: data.reason || "Legitimate domain validated", + detectionMethod: data.validationType || "domain_validation", + category: "validation", + result: data.result || "legitimate", + confidence: data.confidence || 1.0, + context: { + referrer: null, + pageTitle: null, + domain: data.domain || null, + redirectTo: null + } + }; + } + + sanitizeUserProfile(profile) { + if (!profile) return null; + + return { + email: profile.userInfo?.email || null, + id: profile.userInfo?.id || null, + accountType: profile.userInfo?.accountType || "unknown", + provider: profile.userInfo?.provider || "unknown", + isManaged: profile.isManaged || false, + profileId: profile.profileId || null + }; + } + + async sendWebhook(webhookType, data, metadata = {}) { + const webhookConfig = await this.getWebhookConfig(webhookType); + + if (!webhookConfig || !webhookConfig.url) { + return { + success: false, + error: "Webhook not configured", + skipped: true + }; + } + + const payload = webhookConfig.type === "cipp" ? + this.buildCippPayload(data, metadata) : + this.buildPayload(webhookType, data, metadata); + + try { + const response = await fetch(webhookConfig.url, { + method: "POST", + headers: { + "Content-Type": "application/json", + "User-Agent": `Check/${metadata.extensionVersion || chrome.runtime.getManifest().version}`, + "X-Webhook-Type": webhookType, + "X-Webhook-Version": "1.0" + }, + body: JSON.stringify(payload) + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + logger.log(`Webhook sent successfully: ${webhookType}`); + return { + success: true, + status: response.status, + webhookType: webhookType + }; + } catch (error) { + logger.error(`Failed to send webhook ${webhookType}:`, error.message); + return { + success: false, + error: error.message, + webhookType: webhookType + }; + } + } + + buildCippPayload(data, metadata) { + const config = metadata.config || {}; + const userProfile = metadata.userProfile; + + const userEmail = userProfile?.userInfo?.email || null; + const userDisplayName = userProfile?.userInfo?.displayName || + userProfile?.userInfo?.name || + (userEmail ? userEmail.split("@")[0] : null); + + const browserContext = { + browserType: userProfile?.browserInfo?.browserType || "unknown", + browserVersion: userProfile?.browserInfo?.browserVersion || "unknown", + platform: userProfile?.browserInfo?.platform || "unknown", + language: userProfile?.browserInfo?.language || "unknown", + extensionVersion: userProfile?.browserInfo?.version || + chrome.runtime.getManifest().version, + installType: userProfile?.browserInfo?.installType || "unknown" + }; + + return { + ...data, + tenantId: config.cippTenantId || metadata.tenantId || null, + userEmail: userEmail, + userDisplayName: userDisplayName, + accountType: userProfile?.userInfo?.accountType || "unknown", + isManaged: userProfile?.isManaged || false, + profileId: userProfile?.profileId || null, + browserContext: browserContext, + alertSeverity: this.mapSeverityLevel(data.severity || data.threatLevel), + alertCategory: this.categorizeSecurityEvent(data), + detectionMethod: "chrome_extension", + extensionId: chrome.runtime.id, + reportVersion: "2.0", + ...(data.redirectTo && { + redirectContext: { + redirectHost: data.redirectTo, + isLocalhost: data.redirectTo?.includes("localhost"), + isPrivateIP: metadata.isPrivateIP || false + } + }), + ...(data.clientId && { + oauthContext: { + clientId: data.clientId, + appName: data.appName || "Unknown", + ...(data.reason && { threatReason: data.reason }) + } + }) + }; + } + + mapSeverityLevel(severity) { + const severityMap = { + critical: "CRITICAL", + high: "HIGH", + medium: "MEDIUM", + low: "LOW", + info: "INFORMATIONAL" + }; + return severityMap[severity?.toLowerCase()] || "MEDIUM"; + } + + categorizeSecurityEvent(payload) { + const type = payload.type?.toLowerCase() || ""; + + if (type.includes("rogue_app") || payload.ruleType === "rogue_app_detection") { + return "OAUTH_THREAT"; + } + if (type.includes("phishing") || type.includes("blocked")) { + return "PHISHING_ATTEMPT"; + } + if (type.includes("validation")) { + return "VALIDATION_EVENT"; + } + return "SECURITY_EVENT"; + } + + isPrivateIP(host) { + if (!host) return false; + const privateRanges = [ + /^10\./, + /^172\.(1[6-9]|2[0-9]|3[01])\./, + /^192\.168\./, + /^127\./, + /^localhost$/i + ]; + return privateRanges.some(range => range.test(host)); + } +} diff --git a/test-pages/index.html b/test-pages/index.html new file mode 100644 index 00000000..394542bc --- /dev/null +++ b/test-pages/index.html @@ -0,0 +1,90 @@ + + + + + + Check Extension Test Suite + + + +

Check Extension Test Suite

+

Simple test to verify the Check extension's phishing detection.

+ + + + + +
+

Instructions

+
    +
  1. Ensure the Check extension is loaded in your browser
  2. +
  3. Click on each test link to verify detection behavior
  4. +
  5. Critical/High threats should trigger blocking or warning
  6. +
  7. Safe pages should not trigger any alerts
  8. +
  9. Check the extension popup for detection details
  10. +
+
+ + diff --git a/test-pages/phishing-basic.html b/test-pages/phishing-basic.html new file mode 100644 index 00000000..daab85b9 --- /dev/null +++ b/test-pages/phishing-basic.html @@ -0,0 +1,73 @@ + + + + + + Microsoft 365 Sign In + + + + + + + diff --git a/test-pages/safe-page.html b/test-pages/safe-page.html new file mode 100644 index 00000000..ea1d0cd6 --- /dev/null +++ b/test-pages/safe-page.html @@ -0,0 +1,88 @@ + + + + + + Company Portal - Employee Resources + + + +
+

Company Employee Portal

+

Welcome to our internal resources page

+
+ +
+

Employee Resources

+

This page provides quick access to various company resources and tools. No login required for this public information page.

+ + + +

Quick Links

+ + +

+ For secure access to your personal information, please use the official company portal link provided by your IT department. +

+
+ + diff --git a/tests/config-persistence.test.js b/tests/config-persistence.test.js new file mode 100644 index 00000000..b991d5fc --- /dev/null +++ b/tests/config-persistence.test.js @@ -0,0 +1,237 @@ +import { test } from 'node:test'; +import assert from 'node:assert'; +import { setupGlobalChrome, teardownGlobalChrome } from './helpers/chrome-mock.js'; + +test('ConfigManager - custom rules URL persistence', async (t) => { + const chromeMock = setupGlobalChrome(); + + global.fetch = async () => ({ + ok: false, + status: 404 + }); + + const { ConfigManager } = await import('../scripts/modules/config-manager.js'); + + await t.test('should persist custom rules URL after save', async () => { + const configManager = new ConfigManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + await configManager.updateConfig({ + customRulesUrl: customUrl, + updateInterval: 12 + }); + + const localData = chromeMock.storage.local.getData(); + assert.ok(localData.config, 'Config should be saved to local storage'); + assert.strictEqual( + localData.config.customRulesUrl, + customUrl, + 'Custom rules URL should be saved' + ); + }); + + await t.test('should load custom rules URL after reload', async () => { + const configManager = new ConfigManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + await configManager.updateConfig({ + customRulesUrl: customUrl + }); + + const newConfigManager = new ConfigManager(); + const config = await newConfigManager.loadConfig(); + + assert.strictEqual( + config.customRulesUrl, + customUrl, + 'Custom rules URL should persist after reload' + ); + }); + + await t.test('should not save default values to local storage', async () => { + const configManager = new ConfigManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + await configManager.updateConfig({ + customRulesUrl: customUrl + }); + + const localData = chromeMock.storage.local.getData(); + const defaultConfig = configManager.getDefaultConfig(); + + assert.notDeepStrictEqual( + localData.config, + defaultConfig, + 'Local storage should not contain full default config' + ); + + assert.ok( + Object.keys(localData.config).length < Object.keys(defaultConfig).length, + 'Local storage should only contain user overrides' + ); + }); + + await t.test('should preserve custom URL when other settings change', async () => { + const configManager = new ConfigManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + await configManager.updateConfig({ + customRulesUrl: customUrl + }); + + await configManager.updateConfig({ + updateInterval: 48 + }); + + const config = await configManager.getConfig(); + assert.strictEqual( + config.customRulesUrl, + customUrl, + 'Custom URL should be preserved when updating other settings' + ); + }); + + await t.test('should handle empty custom URL correctly', async () => { + const configManager = new ConfigManager(); + + await configManager.updateConfig({ + customRulesUrl: '' + }); + + const config = await configManager.getConfig(); + const defaultUrl = 'https://raw.githubusercontent.com/CyberDrain/Check/refs/heads/main/rules/detection-rules.json'; + + assert.strictEqual( + config.customRulesUrl, + defaultUrl, + 'Empty custom URL should fall back to default' + ); + }); + + delete global.fetch; + teardownGlobalChrome(); +}); + +test('ConfigManager - enterprise policy precedence', async (t) => { + const chromeMock = setupGlobalChrome(); + + global.fetch = async () => ({ + ok: false, + status: 404 + }); + + const { ConfigManager } = await import('../scripts/modules/config-manager.js'); + + await t.test('should override user settings with enterprise policy', async () => { + const configManager = new ConfigManager(); + + const userUrl = 'https://user-custom.com/rules.json'; + await configManager.updateConfig({ + customRulesUrl: userUrl + }); + + const enterpriseUrl = 'https://enterprise.com/rules.json'; + chromeMock.storage.managed.set({ + customRulesUrl: enterpriseUrl + }); + + const newConfigManager = new ConfigManager(); + const config = await newConfigManager.loadConfig(); + + assert.strictEqual( + config.customRulesUrl, + enterpriseUrl, + 'Enterprise policy should override user settings' + ); + }); + + delete global.fetch; + teardownGlobalChrome(); +}); + +test('DetectionRulesManager - configuration reload', async (t) => { + const chromeMock = setupGlobalChrome(); + + global.fetch = async () => ({ + ok: false, + status: 404 + }); + + const { DetectionRulesManager } = await import('../scripts/modules/detection-rules-manager.js'); + + await t.test('should reload configuration when custom URL changes', async () => { + const rulesManager = new DetectionRulesManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + await chromeMock.storage.local.set({ + config: { customRulesUrl: customUrl } + }); + + await rulesManager.reloadConfiguration(); + + assert.strictEqual( + rulesManager.remoteUrl, + customUrl, + 'DetectionRulesManager should use new custom URL after reload' + ); + }); + + await t.test('should use custom URL in forceUpdate', async () => { + const rulesManager = new DetectionRulesManager(); + + const customUrl = 'https://example.com/custom-rules.json'; + + await chromeMock.storage.local.set({ + config: { customRulesUrl: customUrl } + }); + + let fetchedUrl = null; + global.fetch = async (url) => { + fetchedUrl = url; + return { + ok: true, + json: async () => ({ rules: [] }) + }; + }; + + try { + await rulesManager.forceUpdate(); + assert.strictEqual(fetchedUrl, customUrl, 'Should fetch from custom URL'); + } catch (e) { + } + }); + + delete global.fetch; + teardownGlobalChrome(); +}); + +test('ConfigManager - merge precedence', async (t) => { + const chromeMock = setupGlobalChrome(); + + global.fetch = async () => ({ + ok: false, + status: 404 + }); + + const { ConfigManager } = await import('../scripts/modules/config-manager.js'); + + await t.test('should follow correct merge order: default < branding < local < enterprise', async () => { + const configManager = new ConfigManager(); + + const localUrl = 'https://local.com/rules.json'; + await chromeMock.storage.local.set({ + config: { customRulesUrl: localUrl } + }); + + const config = await configManager.loadConfig(); + + assert.strictEqual( + config.customRulesUrl, + localUrl, + 'Local config should override defaults' + ); + }); + + delete global.fetch; + teardownGlobalChrome(); +}); diff --git a/tests/helpers/chrome-mock.js b/tests/helpers/chrome-mock.js new file mode 100644 index 00000000..db01d3cc --- /dev/null +++ b/tests/helpers/chrome-mock.js @@ -0,0 +1,185 @@ +export class ChromeMock { + constructor() { + this.storage = { + local: new LocalStorageMock(), + managed: new ManagedStorageMock(), + session: new SessionStorageMock() + }; + this.runtime = new RuntimeMock(); + } + + reset() { + this.storage.local.clear(); + this.storage.managed.clear(); + this.storage.session.clear(); + this.runtime.reset(); + } +} + +class LocalStorageMock { + constructor() { + this.data = {}; + } + + get(keys) { + return new Promise((resolve) => { + if (typeof keys === 'string') { + resolve({ [keys]: this.data[keys] }); + } else if (Array.isArray(keys)) { + const result = {}; + keys.forEach(key => { + if (key in this.data) { + result[key] = this.data[key]; + } + }); + resolve(result); + } else if (keys === null || keys === undefined) { + resolve({ ...this.data }); + } else { + resolve({}); + } + }); + } + + set(items) { + return new Promise((resolve) => { + Object.assign(this.data, items); + resolve(); + }); + } + + remove(keys) { + return new Promise((resolve) => { + const keyArray = Array.isArray(keys) ? keys : [keys]; + keyArray.forEach(key => delete this.data[key]); + resolve(); + }); + } + + clear() { + this.data = {}; + return Promise.resolve(); + } + + getData() { + return { ...this.data }; + } +} + +class ManagedStorageMock { + constructor() { + this.data = {}; + } + + get(keys) { + return new Promise((resolve) => { + if (keys === null || keys === undefined) { + resolve({ ...this.data }); + } else if (typeof keys === 'string') { + resolve({ [keys]: this.data[keys] }); + } else if (Array.isArray(keys)) { + const result = {}; + keys.forEach(key => { + if (key in this.data) { + result[key] = this.data[key]; + } + }); + resolve(result); + } else { + resolve({}); + } + }); + } + + set(items) { + Object.assign(this.data, items); + } + + clear() { + this.data = {}; + } +} + +class SessionStorageMock { + constructor() { + this.data = {}; + } + + get(keys) { + return new Promise((resolve) => { + if (typeof keys === 'string') { + resolve({ [keys]: this.data[keys] }); + } else if (Array.isArray(keys)) { + const result = {}; + keys.forEach(key => { + if (key in this.data) { + result[key] = this.data[key]; + } + }); + resolve(result); + } else { + resolve({}); + } + }); + } + + set(items) { + return new Promise((resolve) => { + Object.assign(this.data, items); + resolve(); + }); + } + + clear() { + this.data = {}; + } +} + +class RuntimeMock { + constructor() { + this.manifest = { + version: '1.0.5', + name: 'Check' + }; + this.id = 'test-extension-id'; + this.lastError = null; + this.messageListeners = []; + } + + getManifest() { + return this.manifest; + } + + getURL(path) { + return `chrome-extension://${this.id}/${path}`; + } + + sendMessage(message, callback) { + setTimeout(() => { + if (callback) { + callback({ success: true }); + } + }, 0); + } + + onMessage = { + addListener: (listener) => { + this.messageListeners.push(listener); + } + }; + + reset() { + this.lastError = null; + this.messageListeners = []; + } +} + +export function setupGlobalChrome() { + const chromeMock = new ChromeMock(); + global.chrome = chromeMock; + return chromeMock; +} + +export function teardownGlobalChrome() { + delete global.chrome; +} diff --git a/tests/helpers/logger-mock.js b/tests/helpers/logger-mock.js new file mode 100644 index 00000000..78dc7102 --- /dev/null +++ b/tests/helpers/logger-mock.js @@ -0,0 +1,9 @@ +const logger = { + init: () => {}, + log: () => {}, + warn: () => {}, + error: () => {}, + debug: () => {} +}; + +export default logger;