diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..46b47d5 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true +indent_style = space +indent_size = 4 + +[*.{bat,cmd}] +end_of_line = crlf + +[*.{md,yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..bf84a14 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.cmd text eol=crlf +*.ps1 text eol=crlf +*.sh text eol=lf +*.md text eol=lf +*.yml text eol=lf +*.yaml text eol=lf +*.cff text eol=lf +*.txt text eol=lf + +*.png binary +*.jpg binary +*.ico binary + +*.svg text diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..0accf8a --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,65 @@ +name: Bug report +description: An executable is still reaching the internet, or the script errored out. +title: "[bug] " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this. Please attach the transcript log + (`%TEMP%\WinMasterBlocker-*.log`). Reports without the log usually need a + round-trip to triage; with the log, most issues are resolved on first reply. + + - type: input + id: windows-version + attributes: + label: Windows version + description: "Run `winver` and paste the version string (for example, Windows 11 24H2 build 26100.4202)." + placeholder: "Windows 11 24H2 build 26100.xxxx" + validations: + required: true + + - type: input + id: app + attributes: + label: Application and version + description: "The vendor and the specific app + version that is still phoning home." + placeholder: "Adobe Acrobat DC 2026.001.20245" + validations: + required: true + + - type: input + id: exe-path + attributes: + label: Full path to the offending executable + placeholder: "C:\\Program Files\\Adobe\\Acrobat DC\\Acrobat\\acrocef_1\\acrocef.exe" + validations: + required: true + + - type: dropdown + id: ran-update + attributes: + label: After the most recent application update, did you re-run the script (or option 98)? + options: + - "Yes, re-ran after update" + - "No, did not re-run after update" + - "Not sure" + validations: + required: true + + - type: textarea + id: transcript + attributes: + label: Transcript log excerpt + description: "Paste the contents of %TEMP%\\WinMasterBlocker-*.log, or the relevant section." + render: text + validations: + required: false + + - type: textarea + id: extra + attributes: + label: Anything else worth knowing + description: "Custom install path, non-default drive, group policy involvement, etc." + validations: + required: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..bceaa5f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,173 @@ +name: ci + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: false + +jobs: + lint: + name: lint + format + audit + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: lint + run: bash tools/lint-bat.sh WinMasterBlocker.bat + + - name: format-check + run: bash tools/format-check.sh WinMasterBlocker.bat + + - name: audit-coverage + run: bash tools/audit-coverage.sh WinMasterBlocker.bat + + integration: + name: WHATIF integration on Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v6 + + - name: stage fake vendor tree + shell: pwsh + run: | + $root = Join-Path $env:RUNNER_TEMP 'wmb-fake' + New-Item -ItemType Directory -Force -Path $root | Out-Null + + $adobe = Join-Path $root 'Program Files\Adobe\Acrobat DC\Acrobat\acrocef_1' + New-Item -ItemType Directory -Force -Path $adobe | Out-Null + New-Item -ItemType File -Force -Path (Join-Path $adobe 'acrocef.exe') | Out-Null + + $rdr = Join-Path $root 'Program Files\Adobe\Reader DC\Reader' + New-Item -ItemType Directory -Force -Path $rdr | Out-Null + New-Item -ItemType File -Force -Path (Join-Path $rdr 'RdrCEF.exe') | Out-Null + + New-Item -ItemType Directory -Force -Path (Join-Path $root 'Program Files (x86)\Adobe') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $root 'Common Files\Adobe') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $root 'Common Files (x86)\Adobe') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $root 'ProgramData\Adobe') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $root 'AppData\Local\Adobe') | Out-Null + New-Item -ItemType Directory -Force -Path (Join-Path $root 'AppData\Roaming\Adobe') | Out-Null + + "FAKE_ROOT=$root" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + + - name: run script in WHATIF mode + shell: cmd + env: + WHATIF: "1" + WMB_VENDOR: "Adobe" + WMB_QUIET: "1" + # WMB_TEST_ROOT redirects every path lookup to the fake tree we + # staged above. Cleaner than trying to override %ProgramFiles% + # from the workflow: GitHub Actions Windows runners ignore + # workflow-level overrides for well-known Windows system vars. + WMB_TEST_ROOT: ${{ runner.temp }}\wmb-fake + run: | + echo 0& WinMasterBlocker.bat + + - name: assert rules emitted in transcript + shell: pwsh + run: | + $log = Get-ChildItem $env:TEMP -Filter 'WinMasterBlocker-*.log' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + if (-not $log) { Write-Error 'no transcript log produced'; exit 1 } + Write-Host "transcript: $($log.FullName)" + $body = Get-Content $log.FullName -Raw + Write-Host "----- transcript -----" + Write-Host $body + Write-Host "----- /transcript -----" + # Tightened: require an actual rule emission, not just a path walk. + if ($body -notmatch 'add ".*acrocef.*Adobe-block"') { Write-Error 'transcript missing acrocef rule emission'; exit 1 } + if ($body -notmatch 'add ".*RdrCEF.*Adobe-block"') { Write-Error 'transcript missing RdrCEF rule emission'; exit 1 } + if ($body -notmatch 'WHATIF') { Write-Error 'transcript missing WHATIF marker'; exit 1 } + Write-Host 'integration ok' + + release: + name: GitHub Release on version bump + needs: [lint, integration] + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Extract WMB_VERSION + id: ver + run: | + V=$(grep -oE 'WMB_VERSION=[0-9]+\.[0-9]+\.[0-9]+' WinMasterBlocker.bat | head -1 | cut -d= -f2) + if [ -z "$V" ]; then + echo "::error::WMB_VERSION not found in WinMasterBlocker.bat" + exit 1 + fi + echo "version=$V" >> "$GITHUB_OUTPUT" + echo "tag=v$V" >> "$GITHUB_OUTPUT" + echo "Script version: $V" + + - name: Skip if release already exists + id: check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + if gh release view "${{ steps.ver.outputs.tag }}" >/dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + echo "Release ${{ steps.ver.outputs.tag }} already exists, skipping." + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Verify CITATION.cff version matches + if: steps.check.outputs.exists == 'false' + run: | + CFF=$(grep -oE '^version: "?[0-9]+\.[0-9]+\.[0-9]+"?' CITATION.cff | head -1 | awk '{print $2}' | tr -d '"') + if [ "$CFF" != "${{ steps.ver.outputs.version }}" ]; then + echo "::error::CITATION.cff version ($CFF) does not match WMB_VERSION (${{ steps.ver.outputs.version }})" + exit 1 + fi + + - name: SHA-256 checksums + if: steps.check.outputs.exists == 'false' + run: | + sha256sum WinMasterBlocker.bat LICENSE CITATION.cff > SHA256SUMS.txt + cat SHA256SUMS.txt + + - name: Release notes since previous tag + if: steps.check.outputs.exists == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PREV=$(gh release list --limit 1 --json tagName -q '.[0].tagName' 2>/dev/null || true) + { + echo "## Changes since ${PREV:-initial}" + echo + if [ -n "$PREV" ]; then + git log --pretty='* %s (%h)' "${PREV}..HEAD" + else + git log --pretty='* %s (%h)' + fi + } > RELEASE_NOTES.md + cat RELEASE_NOTES.md + + - name: Publish release + if: steps.check.outputs.exists == 'false' + uses: softprops/action-gh-release@v3 + with: + tag_name: ${{ steps.ver.outputs.tag }} + name: WinMasterBlocker ${{ steps.ver.outputs.tag }} + body_path: RELEASE_NOTES.md + files: | + WinMasterBlocker.bat + LICENSE + CITATION.cff + SHA256SUMS.txt + fail_on_unmatched_files: true + make_latest: true diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml new file mode 100644 index 0000000..3340855 --- /dev/null +++ b/.github/workflows/e2e.yml @@ -0,0 +1,272 @@ +name: e2e + +# Real end-to-end test on a windows-latest runner. Drops WHATIF, runs the +# script in real-mode, creates actual Windows Firewall rules, verifies via +# Get-NetFirewallRule, exercises the menu, delete, idempotency, WHATIF, and +# the known-CEF sweep against a custom drive. Manual trigger only — heavy. +# +# To trigger: +# gh workflow run e2e.yml --ref maintenance-2026-05 +# or use the "Run workflow" button on the Actions tab. Set `enable_tmate=true` +# if you want an interactive SSH session opened just before cleanup. + +on: + workflow_dispatch: + inputs: + enable_tmate: + description: "Open a tmate SSH session before cleanup (interactive)" + type: boolean + default: false + tmate_only: + description: "Skip all tests, just open a tmate session immediately" + type: boolean + default: false + # Auto-run on PRs that touch the script or this workflow. Manual + # triggers via workflow_dispatch only register against the default + # branch, so this is how we run real-mode tests before merging. + pull_request: + branches: [main] + paths: + - "WinMasterBlocker.bat" + - ".github/workflows/e2e.yml" + +permissions: + contents: read + +concurrency: + group: e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + e2e: + name: End-to-end on real Windows + runs-on: windows-latest + timeout-minutes: 30 + steps: + - uses: actions/checkout@v6 + + - name: Tmate (immediate, if requested) + if: ${{ inputs.tmate_only }} + uses: mxschmitt/action-tmate@v3 + with: + detached: false + + - name: Pre-clean any stale *-block rules from previous runs + shell: pwsh + run: | + $stale = Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue + if ($stale) { + Write-Host "Removing $($stale.Count) stale rule(s)" + $stale | Remove-NetFirewallRule + } else { + Write-Host "No stale rules" + } + + - name: Stage fake install trees + shell: pwsh + run: | + # Empty fake tree for WMB_TEST_ROOT (keeps the main path-table walk + # from picking up unrelated runner binaries). + $fake = Join-Path $env:RUNNER_TEMP 'wmb-fake' + New-Item -ItemType Directory -Force -Path $fake | Out-Null + foreach ($sub in @( + 'Program Files\Adobe', + 'Program Files (x86)\Adobe', + 'Common Files\Adobe', + 'Common Files (x86)\Adobe', + 'ProgramData\Adobe', + 'AppData\Local\Adobe', + 'AppData\Roaming\Adobe' + )) { + New-Item -ItemType Directory -Force -Path (Join-Path $fake $sub) | Out-Null + } + + # Custom-drive install: D:\Adobe\ is what the new known-sweep alphabet + # walk catches. We seed acrocef.exe and RdrCEF.exe here so the only + # path that can produce rules is the sweep. + $custom = 'D:\Adobe\Acrobat DC\Acrobat\acrocef_1' + New-Item -ItemType Directory -Force -Path $custom | Out-Null + fsutil file createnew (Join-Path $custom 'acrocef.exe') 1024 | Out-Null + + $rdr = 'D:\Adobe\Reader DC\Reader' + New-Item -ItemType Directory -Force -Path $rdr | Out-Null + fsutil file createnew (Join-Path $rdr 'RdrCEF.exe') 1024 | Out-Null + + # User-profile install (second known-sweep target). Acrobat.exe is + # in the script's known-exe list; the sweep matches by literal name. + $up = Join-Path $env:USERPROFILE 'Adobe\Acrobat\bin' + New-Item -ItemType Directory -Force -Path $up | Out-Null + fsutil file createnew (Join-Path $up 'Acrobat.exe') 1024 | Out-Null + + "FAKE_ROOT=$fake" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "FAKE_ROOT=$fake" + Write-Host "Custom drive seeded: D:\Adobe\..." + Write-Host "User-profile seeded: $up" + + # --------------------------------------------------------------------- + # SCENARIO 1 — real block via unattended mode. + # --------------------------------------------------------------------- + - name: "S1: block creates real firewall rules" + if: ${{ !inputs.tmate_only }} + shell: cmd + env: + WMB_VENDOR: "Adobe" + WMB_QUIET: "1" + WMB_TEST_ROOT: ${{ runner.temp }}\wmb-fake + run: WinMasterBlocker.bat + + - name: "S1: verify acrocef Adobe-block exists (in + out)" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $rules = Get-NetFirewallRule -DisplayName 'acrocef Adobe-block' -ErrorAction SilentlyContinue + if ($rules.Count -ne 2) { + Write-Host "::error::expected 2 rules (in+out) for acrocef, got $($rules.Count)" + $all = Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue + Write-Host "All *-block rules on host:" + $all | Select-Object DisplayName,Direction,Action | Format-Table + exit 1 + } + foreach ($r in $rules) { + if ($r.Action -ne 'Block') { + Write-Host "::error::rule $($r.DisplayName) dir=$($r.Direction) action=$($r.Action), expected Block" + exit 1 + } + } + Write-Host "S1 ok: 2 acrocef rules, both Block" + + - name: "S1: verify RdrCEF + Acrobat (known-sweep proof)" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $missing = @() + foreach ($name in @('RdrCEF Adobe-block','Acrobat Adobe-block')) { + $rules = Get-NetFirewallRule -DisplayName $name -ErrorAction SilentlyContinue + if ($rules.Count -ne 2) { + $missing += "$name (got $($rules.Count))" + } + } + if ($missing.Count -gt 0) { + Write-Host "::error::known-sweep regression: $($missing -join '; ')" + Write-Host "All *-block rules currently on host:" + Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue | Select-Object DisplayName,Direction,Action | Format-Table + exit 1 + } + Write-Host "S1 ok: known-sweep caught D:\Adobe\ + %USERPROFILE%\Adobe\" + + # --------------------------------------------------------------------- + # SCENARIO 2 — idempotency. Re-running adds zero new rules. + # --------------------------------------------------------------------- + - name: "S2: count rules before re-run" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $n = (Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue).Count + "BEFORE_COUNT=$n" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "BEFORE_COUNT=$n" + + - name: "S2: re-run script (should be idempotent)" + if: ${{ !inputs.tmate_only }} + shell: cmd + env: + WMB_VENDOR: "Adobe" + WMB_QUIET: "1" + WMB_TEST_ROOT: ${{ runner.temp }}\wmb-fake + run: WinMasterBlocker.bat + + - name: "S2: verify count unchanged" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $after = (Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue).Count + if ($after -ne [int]$env:BEFORE_COUNT) { + Write-Host "::error::idempotency broken: before=$env:BEFORE_COUNT after=$after" + exit 1 + } + Write-Host "S2 ok: $after rules (unchanged)" + + # --------------------------------------------------------------------- + # SCENARIO 3 — delete via unattended mode. + # --------------------------------------------------------------------- + - name: "S3: delete all via WMB_ACTION=delete" + if: ${{ !inputs.tmate_only }} + shell: cmd + env: + WMB_VENDOR: "Adobe" + WMB_ACTION: "delete" + WMB_QUIET: "1" + run: WinMasterBlocker.bat + + - name: "S3: verify zero *-block rules remain" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $remaining = Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue + if ($remaining) { + Write-Host "::error::delete left $($remaining.Count) rule(s) behind:" + $remaining | Select-Object DisplayName,Direction | Format-Table + exit 1 + } + Write-Host "S3 ok: all -block rules removed" + + # --------------------------------------------------------------------- + # SCENARIO 4 — WHATIF makes zero firewall changes. + # --------------------------------------------------------------------- + - name: "S4: WHATIF mode creates no rules" + if: ${{ !inputs.tmate_only }} + shell: cmd + env: + WHATIF: "1" + WMB_VENDOR: "Adobe" + WMB_QUIET: "1" + WMB_TEST_ROOT: ${{ runner.temp }}\wmb-fake + run: WinMasterBlocker.bat + + - name: "S4: verify firewall untouched" + if: ${{ !inputs.tmate_only }} + shell: pwsh + run: | + $rules = Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue + if ($rules) { + Write-Host "::error::WHATIF created $($rules.Count) real rule(s)!" + exit 1 + } + $log = Get-ChildItem $env:TEMP -Filter 'WinMasterBlocker-*.log' | Sort-Object LastWriteTime -Descending | Select-Object -First 1 + $body = Get-Content $log.FullName -Raw + if ($body -notmatch '\[WHATIF\] add ".*acrocef.*Adobe-block"') { + Write-Host "::error::WHATIF transcript missing acrocef line" + exit 1 + } + Write-Host "S4 ok: no real rules, transcript has [WHATIF] entries" + + # --------------------------------------------------------------------- + # Cleanup before tmate / exit. Always runs. + # --------------------------------------------------------------------- + # Manual opt-in only. Auto-opening on failure() blocks the job + # waiting for an SSH client; surfacing the transcript log below is + # enough for most diagnoses. + - name: Tmate (debug, before cleanup) + if: ${{ inputs.enable_tmate }} + uses: mxschmitt/action-tmate@v3 + with: + detached: false + limit-access-to-actor: true + + - name: Final cleanup + if: always() + shell: pwsh + run: | + Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue | Remove-NetFirewallRule -ErrorAction SilentlyContinue + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue D:\Adobe + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $env:USERPROFILE 'Adobe') + Remove-Item -Recurse -Force -ErrorAction SilentlyContinue (Join-Path $env:RUNNER_TEMP 'wmb-fake') + Write-Host "Cleanup done." + + - name: Surface transcript log on failure + if: failure() + shell: pwsh + run: | + Get-ChildItem $env:TEMP -Filter 'WinMasterBlocker-*.log' | Sort-Object LastWriteTime -Descending | ForEach-Object { + Write-Host "=== $($_.FullName) ===" + Get-Content $_.FullName -Raw + } diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c094587 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +# Local AI agent instructions (not for public repo) +CLAUDE.md +AGENTS.md +.cursorrules +.aider* + +# Editor / OS +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store +Thumbs.db +desktop.ini + +# Local tooling +node_modules/ +.env +.env.local +*.log + +# Temp files the script itself writes (just in case anyone runs it inside the repo) +WinMasterBlocker-*.log +wmb-uac-*.env +SHA256SUMS.txt +RELEASE_NOTES.md diff --git a/CITATION.cff b/CITATION.cff new file mode 100644 index 0000000..8582946 --- /dev/null +++ b/CITATION.cff @@ -0,0 +1,25 @@ +cff-version: 1.2.0 +title: WinMasterBlocker +message: "If you reference this tool in research, articles, or other software, please cite it using the metadata below." +type: software +authors: + - alias: ph33nx + website: "https://github.com/ph33nx" +repository-code: "https://github.com/ph33nx/WinMasterBlocker" +url: "https://github.com/ph33nx/WinMasterBlocker" +abstract: "A Windows batch script that uses the built-in Windows Firewall command line to block telemetry and license-server traffic from desktop applications such as Adobe Acrobat, Photoshop, Autodesk, Corel and Maxon, with no third-party dependencies." +keywords: + - windows-firewall + - netsh + - adobe-blocker + - acrocef + - firewall-rules + - internet-blocker + - windows-batch + - simplewall-alternative + - outbound-firewall + - application-firewall + - windows-sysadmin +license: MIT +version: 2.0.0 +date-released: "2026-05-11" diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..07d2777 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2024-2026 ph33nx + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 0bb049b..a0499e1 100644 --- a/README.md +++ b/README.md @@ -1,96 +1,145 @@ -### 🟢🔵🔴 Contributions welcome in expanding the list of vendors and installation paths! +# WinMasterBlocker -👉 If you know additional paths for app vendors or have information on other vendors, please consider contributing. (See the **Contributing** section below for details.) +

+ WinMasterBlocker — block Adobe, Autodesk and Corel from the internet using Windows Firewall +

-# 🔥 WinMasterBlocker 🔥 +

+ MIT License + GitHub stars + CI status + Last commit + Windows 10 22H2 / Windows 11 + No dependencies +

-### Your all-in-one firewall control script for blocking Adobe, Corel, Autodesk, Maxon and more from phoning-home! 💻🚫 +> Windows 10 support tracks Microsoft's consumer ESU window and will be reduced to best-effort after October 13, 2026. -## What is this? +A small Windows batch script that uses the built-in Windows Firewall to block Adobe, Autodesk, Corel, Maxon and Red Giant applications from reaching the internet. No drivers, no kernel hooks, no third-party services, no subscription. The script reads a vendor list, walks every install directory it knows about, and adds inbound and outbound `block` rules per executable through `netsh advfirewall`. -Welcome to **WinMasterBlocker**—a nifty little batch script that slaps firewall rules on apps you probably don’t want connecting to the internet (like Adobe, Autodesk, Corel, Maxon and others). It’s your no-fuss way to get peace and quiet from those phoning-home apps. 🚫📡 +If you have ever watched `acrocef.exe`, `RdrCEF.exe` or the Adobe Genuine Service phone home minutes after a fresh install, this is for you. -## !IMP: Only Windows inbuilt commands & firewall cli is used to block the apps from internet access, NO third party tools or solutions are used for this script's functionality. +## What it blocks ---- +| Vendor | Why you might want this | Common process names blocked | +| --- | --- | --- | +| **Adobe** | License-server polling, telemetry, Creative Cloud sync, Acrobat CEF children | `acrocef.exe`, `RdrCEF.exe`, `Acrobat.exe`, `AcroRd32.exe`, `AdobeNotificationClient.exe`, `AdobeIPCBroker.exe`, `Creative Cloud.exe` | +| **Autodesk** | Single-sign-on heartbeats, AutoCAD telemetry, license-manager polling | every `*.exe` under `Autodesk\` and `Autodesk Shared\` | +| **Corel** | Activation server checks, update prompts | every `*.exe` under `Corel\` | +| **Maxon** | Cinema 4D license polling, render-node check-ins | every `*.exe` under `Maxon\` | +| **Red Giant** | Trapcode license heartbeat, Universe activation | every `*.exe` under `Red Giant\` | -## Quick Start +Coverage is two-layered. First a recursive walk of every known install path (`%ProgramFiles%\Adobe`, `%LOCALAPPDATA%\Adobe`, `%APPDATA%\Adobe` and the rest). Then a vendor-specific known-process sweep that scans every other logical drive's `\Adobe\` root and `%USERPROFILE%\Adobe\` for the specific binaries that have historically slipped past — covers custom installs to `D:\Adobe\` or any other non-default drive the main walk cannot reach. -To run the script, simply download `WinMasterBlocker.bat` and **double-click** it or run the following command from the command line **as administrator**: +## Quickstart -```bash -WinMasterBlocker.bat -``` +1. Download `WinMasterBlocker.bat` from this repo. +2. Right-click it, **Run as administrator**. If you double-click, the script re-launches itself with elevation through PowerShell. +3. Pick a vendor from the menu. Pick `98` after a vendor update to re-scan and add any new executables. Pick `99` to remove every rule the script added. ---- +A transcript log lands at `%TEMP%\WinMasterBlocker-YYYYMMDDhhmmss.log`. Every check, skip and rule add is recorded there. Most "it did not work" reports are answered by reading the last 50 lines of that file. -## Usage 📜 +## How it compares -### Running the script: +| | **WinMasterBlocker** | SimpleWall | NetLimiter | GlassWire | +| --- | --- | --- | --- | --- | +| License | MIT | LGPL | Commercial | Commercial | +| Price | Free | Free | $30 / 2 yr | $39 / yr | +| Third-party driver | None | WFP filter driver | WFP filter driver | WFP filter driver | +| Per-application rules | Yes (per `.exe`) | Yes | Yes | Yes | +| GUI | None (menu only) | Yes | Yes | Yes | +| Profile-aware (Domain / Private / Public) | No (rule applies to all) | Yes | Yes | Yes | +| Scriptable / unattended | Yes (env vars) | Limited | Yes | No | +| Auditable in plain text | Yes (17 KB single file) | No | No | No | +| Survives reboot | Yes (Windows Firewall persists) | Yes | Yes | Yes | +| Survives uninstall of the tool | Yes (rules are native) | No | No | No | -1. **Make sure you run this as admin!** It won't work otherwise _(netsh requires it)_. We’ll prompt you if you forget, don’t worry. 😎 -2. **Double-click or run from the command line** +WinMasterBlocker is for the case where you want firewall rules and only firewall rules: visible in `wf.msc`, exportable through `netsh advfirewall export`, removable through Group Policy, the same shape an enterprise admin would write by hand. If you want a packet log, an outbound prompt UI or per-NIC bandwidth shaping, use one of the commercial tools above. -### Options in the menu: +## FAQ -- **Block rules** for Adobe, Corel, Autodesk, etc. -- **Remove firewall rules** (just the ones we added, don’t worry 😉). -- You can pick whether to block **inbound**, **outbound**, or both kinds of traffic. Customizable and clean. 💪 +### Is this safe to run? ---- +Yes. The script only invokes `net session`, `netsh advfirewall firewall add rule`, `netsh advfirewall firewall delete rule`, and a single PowerShell call to enumerate existing rules. No registry edits, no service installs, no driver loads, no scheduled tasks. Every action is written to the transcript log before it is executed, and every change is reversible from the Windows Firewall UI (`wf.msc`) or by running this script with menu option `99`. The full source is one batch file, around 450 lines, that you can read top to bottom in five minutes. There is no telemetry, no auto-update, and no network call from the script itself. -## Contributing 👾 +### Will Adobe still activate after I run this? -We love contributions, PRs, and feature requests! If you’re one of those who likes to tinker and hack on scripts, here’s how you can get involved: +Yes, with one caveat. Adobe activation reaches the network through `Adobe Desktop Service.exe` and `node.exe` under `Creative Cloud`, both of which the recursive walk will block. Run the script first, complete activation while temporarily disabling the relevant rules, then re-enable them. The simpler path for most users: install and activate first, then run WinMasterBlocker. Subsequent re-activations after license changes will need the same temporary unblock. -### Steps to contribute: +### Why is `acrocef.exe` still connecting after I ran the script? -1. **Fork it** 🍴 – You know the drill. Fork this repo. -2. **Clone it** 🛠️ – Get the code to your local: - ```bash - git clone https://github.com/ph33nx/WinMasterBlocker - ``` -3. **Create a branch** 🌿 – New features? Fixes? Start a new branch: - ```bash - git checkout -b my-cool-feature - ``` -4. **Add your magic** ✨ – Modify `WinMasterBlocker.bat` or enhance the `README.md`. Make sure your changes are 💯 legit. -5. **Push your branch** 🚀 – Send it back up: - ```bash - git push origin my-cool-feature - ``` -6. **Submit a Pull Request (PR)** 🤙 +Three causes account for almost every report. First, Acrobat was installed or updated after the last run; re-run the script, or pick option `98` (Update Adobe). Second, Adobe placed a new CEF child under `%LOCALAPPDATA%\Adobe\` that the previous version of WinMasterBlocker did not walk; this version walks both `%LOCALAPPDATA%` and `%APPDATA%`. Third, the previous version made a slow PowerShell duplicate-check call per executable and large Adobe installs took five minutes or more to finish; users killed the run before AcroCEF was reached. The 2.0 release replaces that with a single up-front rule cache, so the Adobe walk completes in seconds. -### Want to add more apps to block? 🛑 +### How do I undo a block? -Feel free to throw in more providers. Just add the app to the `providers[]` and `paths[]` arrays in the script. Got new paths for a provider? Add them in there too! We’re open to adding pretty much anything (the bigger the blacklist, the better). 💣 +Run `WinMasterBlocker.bat` and choose option `99` from the menu. You then pick whether to delete inbound rules, outbound rules, or both. The script only removes rules whose display name ends in `-block`, which is the suffix it appends to every rule it creates, so it will not touch firewall rules added by other tools or by Windows itself. You can also remove rules from the GUI (`wf.msc`, sort by Name, find rules ending in `-block`) or with `netsh advfirewall firewall delete rule name=all dir=out` if you want a fresh start across every outbound rule on the machine. -```batch -:: Add new provider paths here -set "vendors[7]=NewApp" -set "paths[7]=C:\Program Files\NewApp;C:\Program Files (x86)\NewApp" +### Does the block survive Adobe updates? + +The rules survive. New executables introduced by an update do not get blocked automatically, because the rules target specific paths. After an Adobe Acrobat or Creative Cloud update, run the script again and pick option `98` (Update Adobe). The cache-aware duplicate detection means existing rules are skipped instantly; only new executables get new rules. The transcript log lists every rule added on that pass, which makes it easy to confirm that the new CEF children from the update are now blocked. + +### Will it block Windows Update or Microsoft Defender? + +No. The vendor list contains Adobe, Corel, Autodesk, Maxon and Red Giant only. The recursive walks happen under `%ProgramFiles%\\`, `%ProgramFiles(x86)%\\`, `%CommonProgramFiles%\\`, `%ProgramData%\\`, `%LOCALAPPDATA%\\` and `%APPDATA%\\`. Nothing under `%WINDIR%`, `%SystemRoot%`, `%ProgramFiles%\WindowsApps` or `%ProgramFiles%\Windows Defender` is touched. If a vendor binary is somehow installed inside one of those locations, the script will walk it; that is rare and would be a packaging mistake worth a separate report. + +## Troubleshooting + +The transcript log at `%TEMP%\WinMasterBlocker-YYYYMMDDhhmmss.log` records every action. If the script "did nothing", open the latest log: it will show either `path missing` for every Adobe path (you do not have Adobe installed where the script expected, or the elevation switched user contexts and lost `%LOCALAPPDATA%`), or `skip ""` for every executable (the rules already exist from a previous run), or a list of `add` lines (it worked, the rules are now in `wf.msc`). + +Set `WHATIF=1` in the environment before running to see what the script would do without making any firewall changes. Useful as a dry run after editing the script or paths. + +## For IT pros and unattended use + +Four environment variables drive an unattended run from a deployment script, MDM payload or scheduled task: + +```cmd +set WHATIF=1 +set WMB_VENDOR=Adobe +set WMB_ACTION=block +set WMB_QUIET=1 +WinMasterBlocker.bat ``` -### Checklist before sending that PR 🚧: +`WHATIF=1` logs every netsh call without executing it (dry run). `WMB_ACTION=delete` removes every rule added by the script. `WMB_QUIET=1` suppresses per-rule console output (the transcript log is still written). All four survive the UAC re-launch when the script elevates itself on double-click. + +To export the resulting rules into a `.wfw` file you can re-import on other machines, or push through Group Policy: + +```cmd +netsh advfirewall export "C:\share\winmasterblocker-rules.wfw" +``` -- Make sure the script **runs on your machine** before submitting. Nobody likes broken code. 🛠️ -- Follow the existing format for adding new providers or paths. -- Respect the 💀 rule: **no hard-breaking changes**. +The exported file is a binary blob that `netsh advfirewall import` understands on any Windows 10 or 11 host. ---- +## Contributing + +Contributions for new vendors and new install paths are welcome. + +```bash +git clone https://github.com/ph33nx/WinMasterBlocker +cd WinMasterBlocker +# Install lefthook (https://lefthook.dev), then: +lefthook install +``` + +The pre-commit hook runs `tools/lint-bat.sh`, `tools/format-check.sh` and `tools/audit-coverage.sh` against `WinMasterBlocker.bat`. The audit script asserts that the Adobe known-CEF list still contains `acrocef.exe`, `RdrCEF.exe` and the other binaries that earlier issues identified, so the regression that originally inspired #6 cannot silently come back. + +A new vendor: + +```batch +set "vendors[5]=NewVendor" +set "paths[5]=%ProgramFiles%\NewVendor;%ProgramFiles(x86)%\NewVendor;%LOCALAPPDATA%\NewVendor" +``` -## License ⚖️ +A new known-bad executable for an existing vendor: append it to the relevant `for %%E in (...)` list in the `:adobe_sweep_root` helper (or the equivalent for other vendors as they grow) and add it to `REQUIRED_ADOBE_EXES` in `tools/audit-coverage.sh` so future commits cannot drop it. -MIT License – this means you can do pretty much anything with this, but we’d love a shout-out if you find it useful. 🎉 +## Citation ---- +A `CITATION.cff` is included; GitHub renders a "Cite this repository" button on the right sidebar. If you reference this tool in articles or research, please cite the repository directly rather than copy-pasting the script. -## Contact 📫 +## License -- **Author:** [ph33nx](https://github.com/ph33nx) -- **Current Repo:** [WinMasterBlocker](https://github.com/ph33nx/WinMasterBlocker) -- **Contributions welcomed!** - Especially for app vendors and their install locations. +MIT. See [`LICENSE`](LICENSE). SPDX identifier `MIT` is set in the script header. ---- +## Author -Feel free to copy, edit, or just stare at the code. 😎 +[@ph33nx](https://github.com/ph33nx) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..ea59e45 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,13 @@ +# Security policy + +WinMasterBlocker is a single batch script that runs locally and only invokes built-in Windows commands (`net`, `netsh`, `powershell`). It does not install services, drivers, scheduled tasks, or auto-update components. There is no network surface to exploit. + +## Reporting + +If you find a way that the script can be coerced into adding firewall rules against a target an attacker controls, dropping privileges incorrectly, or otherwise behaving in a way that could harm the user, please open a GitHub issue with the `security` label, or contact the author through their GitHub profile. + +## Out of scope + +- Reports that "blocking these applications might prevent legitimate updates". That is the intended behaviour. See the README FAQ. +- Reports that the script requires Administrator. `netsh advfirewall` requires it; this is a Windows requirement, not a design choice. +- Reports that the firewall rules can be removed by an Administrator. Yes, any user with Administrator can edit firewall rules. This script is one of many such users. diff --git a/WinMasterBlocker.bat b/WinMasterBlocker.bat index 2d954b7..1b5e160 100644 --- a/WinMasterBlocker.bat +++ b/WinMasterBlocker.bat @@ -1,250 +1,447 @@ -:: ################################################################ -:: ## 🔥 WinMasterBlocker 🔥 # -:: ################################################################ -:: # Author: https://github.com/ph33nx # -:: # Repo: https://github.com/ph33nx/WinMasterBlocker # -:: # # -:: # This script blocks inbound/outbound network access # -:: # for major apps like Adobe, Autodesk, Corel, Maxon, # -:: # and more using Windows Firewall. # -:: # # -:: # Features: # -:: # - Block executables using windows firewall for popular # -:: # vendors # -:: # - Add or Delete inbound, outbound, or both types of rules # -:: # - Avoids duplicate firewall rules # -:: # - Logs skipped entries for existing rules # -:: # # -:: # Check out the repo to contribute: # -:: # https://github.com/ph33nx/WinMasterBlocker # -:: ################################################################ - -@echo off -setlocal enabledelayedexpansion - -:: Array of vendors and their paths -set "vendors[0]=Adobe" -set "paths[0]=C:\Program Files\Adobe;C:\Program Files\Common Files\Adobe;C:\Program Files (x86)\Adobe;C:\Program Files (x86)\Common Files\Adobe;C:\ProgramData\Adobe" - -set "vendors[1]=Corel" -set "paths[1]=C:\Program Files\Corel;C:\Program Files\Common Files\Corel;C:\Program Files (x86)\Corel" - -set "vendors[2]=Autodesk" -set "paths[2]=C:\Program Files\Autodesk;C:\Program Files (x86)\Common Files\Macrovision Shared;C:\Program Files (x86)\Common Files\Autodesk Shared" - -set "vendors[3]=Maxon" -set "paths[3]=C:\Program Files\Maxon;C:\Program Files (x86)\Maxon;C:\ProgramData\Maxon" - -set "vendors[4]=Red Giant" -set "paths[4]=C:\Program Files\Red Giant;C:\Program Files (x86)\Red Giant" - -:: Check if script is run as administrator -:check_admin - net session >nul 2>&1 - if %errorlevel% neq 0 ( - echo. - echo This script must be run as Administrator. - echo Attempting to re-launch with elevated privileges... - powershell -Command "Start-Process '%~f0' -Verb RunAs" - exit /b - ) - -:: If admin, proceed with script -echo Running with Administrator privileges... -goto menu - -:: Main menu for user selection -:menu -cls -echo Choose a vendor to block or delete rules: -echo. - -:: Iterate through defined vendors -set i=0 -:vendor_loop -if not defined vendors[%i%] goto after_vendor_list -echo !i!: !vendors[%i%]! -set /a i+=1 -goto vendor_loop - -:after_vendor_list -echo 99: Delete all firewall rules (added by this script) -echo. - -set /p "choice=Enter your choice (0-99): " - -:: Validate if choice is a number between 0 and 99 -set /a test_choice=%choice% 2>nul -if "%choice%" neq "%test_choice%" ( - echo Invalid input, please enter a valid number. - pause - goto menu -) - -:: Dynamic input validation based on the number of vendors -set max_choice=!i! -if "%choice%"=="00" ( - goto end -) else if "%choice%"=="99" ( - goto delete_menu -) else if %choice% lss %max_choice% ( - goto process_vendor -) else ( - echo Invalid choice, try again. - pause - goto menu -) - -:: Menu for deleting rules (inbound, outbound, both) -:delete_menu -cls -echo Select which firewall rules to DELETE (added by this script): -echo 1: Delete Outbound rules -echo 2: Delete Inbound rules -echo 3: Delete All -echo. - -set /p "delete_choice=Enter your choice (1-3): " -if "%delete_choice%"=="1" ( - goto delete_outbound -) else if "%delete_choice%"=="2" ( - goto delete_inbound -) else if "%delete_choice%"=="3" ( - goto delete_both -) else ( - echo Invalid choice, try again. - pause - goto delete_menu -) - -:: Delete Outbound rules -:delete_outbound -cls -echo Deleting all outbound firewall rules (added by this script)... -for /f "tokens=*" %%r in ('powershell -command "(Get-NetFirewallRule | where {$_.DisplayName -like '*-block'}).DisplayName"') do ( - for %%D in (out) do ( - netsh advfirewall firewall delete rule name="%%r" dir=%%D - ) -) -echo Outbound rules deleted successfully. -goto firewall_check - -:: Delete Inbound rules -:delete_inbound -cls -echo Deleting all inbound firewall rules (added by this script)... -for /f "tokens=*" %%r in ('powershell -command "(Get-NetFirewallRule | where {$_.DisplayName -like '*-block'}).DisplayName"') do ( - for %%D in (in) do ( - netsh advfirewall firewall delete rule name="%%r" dir=%%D - ) -) -echo Inbound rules deleted successfully. -goto firewall_check - -:: Delete Both Inbound and Outbound rules -:delete_both -cls -echo Deleting all inbound and outbound firewall rules (added by this script)... -for /f "tokens=*" %%r in ('powershell -command "(Get-NetFirewallRule | where {$_.DisplayName -like '*-block'}).DisplayName"') do ( - for %%D in (in out) do ( - netsh advfirewall firewall delete rule name="%%r" dir=%%D - ) -) -echo Inbound and Outbound rules deleted successfully. -goto firewall_check - -:: Process each vendor's paths and block executables -:process_vendor -cls -set "selected_vendor=!vendors[%choice%]!" -set "selected_paths=!paths[%choice%]!" - -:: Initialize rule counter and a flag to track if any valid path was found -set "rule_count=0" -set "any_valid_path=false" - -echo Blocking executables for %selected_vendor% with paths %selected_paths%... - -:: Loop through each path and perform a deep nested search for executables -for %%P in ("%selected_paths:;=" "%") do ( - set "current_path=%%~P" - echo Checking path: "!current_path!" - - if exist "!current_path!" ( - set "any_valid_path=true" - echo Path exists: "!current_path!" - Searching for executables... - - set "exe_found_in_path=false" - - :: Use pushd/popd to make current_path the root of the recursive search - pushd "!current_path!" - for /R %%F in (*.exe) do ( - set "current_exe=%%F" - set "exe_found_in_path=true" - echo Found executable: "!current_exe!" - call :check_and_block "!current_exe!" "!selected_vendor!" - ) - popd - - :: Check if any executables were found in the current path - if "!exe_found_in_path!"=="false" ( - echo No executables found in path: "!current_path!" - ) - - ) else ( - echo Path not found: "!current_path!" - ) -) - -:: Final check after loop - notify if no valid directories were found -if "!any_valid_path!"=="false" ( - echo No valid directories found for %selected_vendor%. -) else if %rule_count%==0 ( - echo No executable files found to block for %selected_vendor%. -) - -echo. -echo Completed blocking for %selected_vendor%. -echo Total rules added: %rule_count% -pause -goto menu - - -:: Function to check if a rule exists, and add it if not -:check_and_block -set "exe_path=%~1" -set "vendor_name=%~2" -set "rule_name=%~n1 %vendor_name%-block" - -echo Checking rule for: "%exe_path%" - -:: Check if the rule already exists -for /f "tokens=*" %%r in ('powershell -command "(Get-NetFirewallRule | where {$_.DisplayName -eq '%rule_name%'}).DisplayName"') do ( - if "%%r"=="%rule_name%" ( - echo Rule for "%exe_path%" already exists, skipping... - goto :continue - ) -) - -:: Add rule if it doesn’t exist -echo Blocking: "%~n1" -netsh advfirewall firewall add rule name="%rule_name%" dir=out program="%exe_path%" action=block -netsh advfirewall firewall add rule name="%rule_name%" dir=in program="%exe_path%" action=block - -:: Increment rule count for each rule added -set /a rule_count+=1 - -:continue -goto :eof - -:: Notify user to check Windows Firewall with Advanced Security -:firewall_check -echo. -echo All changes completed. You can verify the new rules in "Windows Firewall with Advanced Security" -echo. -pause -goto menu - -:end -endlocal -exit /b +:: ################################################################ +:: ## WinMasterBlocker # +:: ################################################################ +:: # Author: https://github.com/ph33nx +:: # Repo: https://github.com/ph33nx/WinMasterBlocker +:: # SPDX-License-Identifier: MIT +:: # Version: 2.0.0 +:: # +:: # Blocks inbound and outbound network access for Adobe, +:: # Autodesk, Corel, Maxon, Red Giant and other vendors using +:: # the Windows Firewall command line. No third-party tools. +:: # +:: # Environment overrides (optional): +:: # WHATIF=1 Log every netsh call instead of executing it. +:: # WMB_VENDOR=adobe Run unattended against a single vendor. +:: # WMB_ACTION=block|delete Action for unattended mode (default: block). +:: # WMB_QUIET=1 Suppress per-rule echo output. +:: # All overrides survive the UAC re-launch when double-clicking the script. +:: # +:: # Transcript log (always written): +:: # %TEMP%\WinMasterBlocker-YYYYMMDDhhmmss.log +:: ################################################################ + +@echo off +setlocal enabledelayedexpansion + +set "WMB_VERSION=2.0.0" + +:: --------------------------------------------------------------------------- +:: UAC handoff restore. When :check_admin re-launches the script with +:: elevation, env vars set in the un-elevated shell (WHATIF, WMB_VENDOR, +:: WMB_TEST_ROOT, ...) do not cross the privilege boundary. The un-elevated +:: side persists them to a temp file matching wmb-uac-*.env and passes the +:: path as %1; we read it back here and delete the file. Validation guards +:: against an unrelated file path being consumed if a user passes %1 by +:: accident. +:: --------------------------------------------------------------------------- +set "_handoff=%~1" +if not defined _handoff goto wmb_after_uac_restore +echo !_handoff! | findstr /I /R "wmb-uac-.*\.env" >nul 2>&1 +if errorlevel 1 goto wmb_after_uac_restore +if not exist "!_handoff!" goto wmb_after_uac_restore +for /f "usebackq tokens=1,* delims==" %%a in ("!_handoff!") do set "%%a=%%b" +del "!_handoff!" 2>nul +set "_handoff=" +:wmb_after_uac_restore + +:: --------------------------------------------------------------------------- +:: Test affordance. WMB_TEST_ROOT lets the integration test point the path +:: table at a fake install tree without requiring admin write to +:: C:\Program Files. Implemented with goto rather than an if-block because +:: cmd's pre-parser counts parens in if-block bodies, and "ProgramFiles(x86)" +:: contains literal parens that confuse the counter. +:: --------------------------------------------------------------------------- +if not defined WMB_TEST_ROOT goto wmb_after_test_root +set "ProgramFiles=%WMB_TEST_ROOT%\Program Files" +set "ProgramFiles(x86)=%WMB_TEST_ROOT%\Program Files (x86)" +set "CommonProgramFiles=%WMB_TEST_ROOT%\Common Files" +set "CommonProgramFiles(x86)=%WMB_TEST_ROOT%\Common Files (x86)" +set "ProgramData=%WMB_TEST_ROOT%\ProgramData" +set "LOCALAPPDATA=%WMB_TEST_ROOT%\AppData\Local" +set "APPDATA=%WMB_TEST_ROOT%\AppData\Roaming" +:wmb_after_test_root + +:: --------------------------------------------------------------------------- +:: Vendor and path table. Paths use environment variables so non-C: installs, +:: x86 / x64 splits, ProgramData, and per-user AppData locations all resolve +:: at runtime. This is what catches Adobe AcroCEF on installs that did not +:: land under C:\Program Files\Adobe. +:: --------------------------------------------------------------------------- +set "vendors[0]=Adobe" +set "paths[0]=%ProgramFiles%\Adobe;%ProgramFiles(x86)%\Adobe;%CommonProgramFiles%\Adobe;%CommonProgramFiles(x86)%\Adobe;%ProgramData%\Adobe;%LOCALAPPDATA%\Adobe;%APPDATA%\Adobe" + +set "vendors[1]=Corel" +set "paths[1]=%ProgramFiles%\Corel;%ProgramFiles(x86)%\Corel;%CommonProgramFiles%\Corel;%CommonProgramFiles(x86)%\Corel;%ProgramData%\Corel" + +set "vendors[2]=Autodesk" +set "paths[2]=%ProgramFiles%\Autodesk;%ProgramFiles(x86)%\Autodesk;%CommonProgramFiles(x86)%\Autodesk Shared;%CommonProgramFiles(x86)%\Macrovision Shared;%ProgramData%\Autodesk" + +set "vendors[3]=Maxon" +set "paths[3]=%ProgramFiles%\Maxon;%ProgramFiles(x86)%\Maxon;%ProgramData%\Maxon" + +set "vendors[4]=Red Giant" +set "paths[4]=%ProgramFiles%\Red Giant;%ProgramFiles(x86)%\Red Giant;%ProgramData%\Red Giant" + +:: --------------------------------------------------------------------------- +:: Transcript log. PowerShell gives us a locale-independent 14-char timestamp +:: that is safe to embed in a filename. wmic is removed in newer Windows 11 +:: builds, so we do not rely on it. +:: --------------------------------------------------------------------------- +set "ts=" +for /f "delims=" %%T in ('powershell -NoProfile -Command "[DateTime]::Now.ToString('yyyyMMddHHmmss')" 2^>nul') do set "ts=%%T" +if not defined ts set "ts=00000000000000" +set "WMB_LOG=%TEMP%\WinMasterBlocker-%ts%.log" +> "%WMB_LOG%" echo WinMasterBlocker v%WMB_VERSION% started %ts% +>>"%WMB_LOG%" echo WHATIF=%WHATIF% WMB_VENDOR=%WMB_VENDOR% WMB_ACTION=%WMB_ACTION% WMB_QUIET=%WMB_QUIET% + +:: --------------------------------------------------------------------------- +:: Admin check. +:: --------------------------------------------------------------------------- +:check_admin + net session >nul 2>&1 + if %errorlevel% neq 0 ( + echo. + echo This script must be run as Administrator. + echo Attempting to re-launch with elevated privileges... + call :write_uac_handoff + powershell -NoProfile -Command "Start-Process '%~f0' -ArgumentList '!_handoff_out!' -Verb RunAs" + exit /b + ) + +echo Running with Administrator privileges... +echo Transcript: %WMB_LOG% + +:: --------------------------------------------------------------------------- +:: Bulk-cache existing firewall rules. One PowerShell call up front replaces +:: the per-executable Get-NetFirewallRule that used to dominate runtime on +:: large Adobe installs (5+ minutes -> seconds). The cache is plain text; +:: duplicate detection is a single findstr against it. +:: --------------------------------------------------------------------------- +set "WMB_RULES_CACHE=%TEMP%\WinMasterBlocker-rules-%ts%.txt" +echo Caching existing firewall rules... +>>"%WMB_LOG%" echo Caching existing firewall rules to %WMB_RULES_CACHE% +powershell -NoProfile -Command "(Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DisplayName)" 2>nul > "%WMB_RULES_CACHE%" +if not exist "%WMB_RULES_CACHE%" type nul > "%WMB_RULES_CACHE%" +call :reload_rule_set + +:: --------------------------------------------------------------------------- +:: Unattended mode for IT pros. Set WMB_VENDOR=adobe (case-insensitive) and +:: optionally WMB_ACTION=block|delete. Skips the menu entirely. +:: --------------------------------------------------------------------------- +if defined WMB_VENDOR ( + call :resolve_vendor "%WMB_VENDOR%" + if not defined WMB_RESOLVED_INDEX ( + echo Unknown WMB_VENDOR: %WMB_VENDOR% + >>"%WMB_LOG%" echo ERROR unknown vendor %WMB_VENDOR% + endlocal + exit /b 2 + ) + set "choice=!WMB_RESOLVED_INDEX!" + if /i "%WMB_ACTION%"=="delete" ( + set "delete_choice=3" + goto delete_both + ) + goto process_vendor +) + +goto menu + +:: --------------------------------------------------------------------------- +:: Main menu. +:: --------------------------------------------------------------------------- +:menu +cls +echo WinMasterBlocker v%WMB_VERSION% +echo Transcript: %WMB_LOG% +if defined WHATIF echo *** WHATIF mode: no firewall changes will be made *** +echo. +echo Choose a vendor to block, or pick a maintenance action: +echo. + +set i=0 +:vendor_loop +if not defined vendors[%i%] goto after_vendor_list +echo !i!: !vendors[%i%]! +set /a i+=1 +goto vendor_loop + +:after_vendor_list +echo. +echo 98: Update Adobe (re-scan after Adobe / Acrobat updates) +echo 99: Delete all firewall rules added by this script +echo 00: Exit +echo. + +set /p "choice=Enter your choice: " + +set /a test_choice=%choice% 2>nul +if "%choice%" neq "%test_choice%" ( + echo Invalid input, please enter a valid number. + pause + goto menu +) + +set max_choice=!i! +if "%choice%"=="00" ( + goto end +) else if "%choice%"=="99" ( + goto delete_menu +) else if "%choice%"=="98" ( + set "choice=0" + goto process_vendor +) else if %choice% lss %max_choice% ( + goto process_vendor +) else ( + echo Invalid choice, try again. + pause + goto menu +) + +:: --------------------------------------------------------------------------- +:: Delete-rules menu. +:: --------------------------------------------------------------------------- +:delete_menu +cls +echo Select which firewall rules to DELETE (added by this script): +echo 1: Delete Outbound rules +echo 2: Delete Inbound rules +echo 3: Delete All +echo 0: Back +echo. + +set /p "delete_choice=Enter your choice (0-3): " +if "%delete_choice%"=="1" ( + goto delete_outbound +) else if "%delete_choice%"=="2" ( + goto delete_inbound +) else if "%delete_choice%"=="3" ( + goto delete_both +) else if "%delete_choice%"=="0" ( + goto menu +) else ( + echo Invalid choice, try again. + pause + goto delete_menu +) + +:delete_outbound +cls +echo Deleting outbound firewall rules added by this script... +call :delete_rules out +goto firewall_check + +:delete_inbound +cls +echo Deleting inbound firewall rules added by this script... +call :delete_rules in +goto firewall_check + +:delete_both +cls +echo Deleting all firewall rules added by this script... +call :delete_rules both +goto firewall_check + +:: dir = "in" | "out" | "both". Reads names from the rule cache so we never +:: touch unrelated rules on the host. Refreshes the cache afterwards. +:delete_rules +set "dir_arg=%~1" +for /f "usebackq delims=" %%r in ("%WMB_RULES_CACHE%") do ( + if not "%%r"=="" ( + if /i "%dir_arg%"=="both" ( + call :run_netsh_delete "%%r" out + call :run_netsh_delete "%%r" in + ) else ( + call :run_netsh_delete "%%r" %dir_arg% + ) + ) +) +echo Refreshing rule cache... +powershell -NoProfile -Command "(Get-NetFirewallRule -DisplayName '*-block' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty DisplayName)" 2>nul > "%WMB_RULES_CACHE%" +if not exist "%WMB_RULES_CACHE%" type nul > "%WMB_RULES_CACHE%" +call :reload_rule_set +goto :eof + +:run_netsh_delete +if defined WHATIF ( + echo [WHATIF] netsh advfirewall firewall delete rule name="%~1" dir=%~2 + >>"%WMB_LOG%" echo [WHATIF] delete rule "%~1" dir=%~2 +) else ( + netsh advfirewall firewall delete rule name="%~1" dir=%~2 >nul + >>"%WMB_LOG%" echo deleted rule "%~1" dir=%~2 +) +goto :eof + +:: --------------------------------------------------------------------------- +:: Process vendor: walk every path, recursively, and block every .exe found. +:: --------------------------------------------------------------------------- +:process_vendor +cls +set "selected_vendor=!vendors[%choice%]!" +set "selected_paths=!paths[%choice%]!" + +set "rule_count=0" +set "any_valid_path=false" + +echo Blocking executables for %selected_vendor% +>>"%WMB_LOG%" echo BEGIN vendor=%selected_vendor% + +for %%P in ("%selected_paths:;=" "%") do ( + set "current_path=%%~P" + if exist "!current_path!" ( + set "any_valid_path=true" + >>"%WMB_LOG%" echo path exists "!current_path!" + if not defined WMB_QUIET echo Searching: "!current_path!" + + pushd "!current_path!" + for /R %%F in (*.exe) do ( + call :check_and_block "%%F" "!selected_vendor!" + ) + popd + ) else ( + >>"%WMB_LOG%" echo path missing "!current_path!" + ) +) + +:: Adobe known-process sweep on non-default install paths (alternative +:: drives, user profile). The recursive walk above covered %ProgramFiles%, +:: %LOCALAPPDATA%, etc. The bulk rule cache means rules already added by +:: the main walk are skipped here via !rule_set! membership. +if /i "%selected_vendor%"=="Adobe" call :adobe_known_sweep + +if "!any_valid_path!"=="false" ( + echo No installation directories found for %selected_vendor%. + >>"%WMB_LOG%" echo END vendor=%selected_vendor% rules=0 reason=no-paths +) else ( + echo. + echo Completed: %selected_vendor%. Rules added: !rule_count! + >>"%WMB_LOG%" echo END vendor=%selected_vendor% rules=!rule_count! +) + +if defined WMB_VENDOR ( + endlocal + exit /b 0 +) + +pause +goto menu + +:: --------------------------------------------------------------------------- +:: Add inbound + outbound block rules for an executable, unless an identically +:: named rule already exists in the cache. +:: --------------------------------------------------------------------------- +:check_and_block +set "exe_path=%~1" +set "vendor_name=%~2" +set "rule_name=%~n1 %vendor_name%-block" + +set "_probe=|%rule_name%|" +if not "!rule_set:%_probe%=!"=="!rule_set!" ( + if not defined WMB_QUIET echo Skip exists: "%~n1" + >>"%WMB_LOG%" echo skip "%rule_name%" + goto :eof +) + +if not defined WMB_QUIET echo Block: "%~n1" +if defined WHATIF ( + echo [WHATIF] netsh advfirewall firewall add rule name="%rule_name%" dir=out program="%exe_path%" action=block + echo [WHATIF] netsh advfirewall firewall add rule name="%rule_name%" dir=in program="%exe_path%" action=block + >>"%WMB_LOG%" echo [WHATIF] add "%rule_name%" out program="%exe_path%" + >>"%WMB_LOG%" echo [WHATIF] add "%rule_name%" in program="%exe_path%" +) else ( + netsh advfirewall firewall add rule name="%rule_name%" dir=out program="%exe_path%" action=block >nul + netsh advfirewall firewall add rule name="%rule_name%" dir=in program="%exe_path%" action=block >nul + >>"%WMB_LOG%" echo add "%rule_name%" program="%exe_path%" +) + +>>"%WMB_RULES_CACHE%" echo %rule_name% +set "rule_set=!rule_set!%rule_name%|" +set /a rule_count+=1 +goto :eof + +:: --------------------------------------------------------------------------- +:: Load WMB_RULES_CACHE into !rule_set! as |name1|name2|...| so per-exe +:: membership in :check_and_block is a substring substitution rather than +:: a per-exe findstr child process. +:: --------------------------------------------------------------------------- +:reload_rule_set +set "rule_set=|" +for /f "usebackq delims=" %%R in ("%WMB_RULES_CACHE%") do set "rule_set=!rule_set!%%R|" +goto :eof + +:: --------------------------------------------------------------------------- +:: Adobe-specific belt-and-suspenders sweep. Walks roots the standard path +:: table cannot reach: every logical drive's \Adobe\ folder (custom installs +:: to D:\Adobe\ etc) and %USERPROFILE%\Adobe (some Creative Cloud components +:: drop here). For each root, looks only for the specific binaries that have +:: historically slipped past the recursive walk on non-default installs. +:: --------------------------------------------------------------------------- +:adobe_known_sweep +>>"%WMB_LOG%" echo BEGIN known-sweep vendor=Adobe +for %%D in (C D E F G H I J K L M N O P Q R S T U V W X Y Z) do ( + if exist "%%D:\Adobe\" call :adobe_sweep_root "%%D:\Adobe" +) +if exist "%USERPROFILE%\Adobe\" call :adobe_sweep_root "%USERPROFILE%\Adobe" +>>"%WMB_LOG%" echo END known-sweep vendor=Adobe +goto :eof + +:adobe_sweep_root +set "_root=%~1" +>>"%WMB_LOG%" echo known-sweep scanning "!_root!" +for %%E in ("acrocef.exe" "RdrCEF.exe" "Acrobat.exe" "AcroRd32.exe" "AdobeNotificationClient.exe" "AdobeIPCBroker.exe" "AGSService.exe" "AdobeUpdateService.exe" "Creative Cloud.exe") do ( + for /f "delims=" %%P in ('dir /b /s /a-d "!_root!\%%~E" 2^>nul') do ( + call :check_and_block "%%P" "Adobe" + ) +) +goto :eof + +:: --------------------------------------------------------------------------- +:: Persist defined env vars to a temp file before UAC re-launch. The elevated +:: side reads the file via the wmb_after_uac_restore block at the top of the +:: script. Sets _handoff_out to the file path so the caller can pass it. +:: --------------------------------------------------------------------------- +:write_uac_handoff +set "_handoff_out=%TEMP%\wmb-uac-%RANDOM%-%RANDOM%.env" +> "!_handoff_out!" type nul +if defined WHATIF >>"!_handoff_out!" echo WHATIF=!WHATIF! +if defined WMB_VENDOR >>"!_handoff_out!" echo WMB_VENDOR=!WMB_VENDOR! +if defined WMB_ACTION >>"!_handoff_out!" echo WMB_ACTION=!WMB_ACTION! +if defined WMB_QUIET >>"!_handoff_out!" echo WMB_QUIET=!WMB_QUIET! +if defined WMB_TEST_ROOT >>"!_handoff_out!" echo WMB_TEST_ROOT=!WMB_TEST_ROOT! +goto :eof + +:: --------------------------------------------------------------------------- +:: Resolve a vendor name (case-insensitive) to its index. Sets +:: WMB_RESOLVED_INDEX if found, otherwise leaves it undefined. +:: --------------------------------------------------------------------------- +:resolve_vendor +set "WMB_RESOLVED_INDEX=" +set "v_query=%~1" +set j=0 +:rv_loop +if not defined vendors[%j%] goto :eof +set "v_curr=!vendors[%j%]!" +if /i "!v_curr!"=="!v_query!" ( + set "WMB_RESOLVED_INDEX=%j%" + goto :eof +) +set /a j+=1 +goto rv_loop + +:firewall_check +echo. +echo Done. Verify in "Windows Firewall with Advanced Security". +echo Transcript: %WMB_LOG% +echo. +if not defined WMB_VENDOR pause +if defined WMB_VENDOR ( + endlocal + exit /b 0 +) +goto menu + +:end +>>"%WMB_LOG%" echo exit +endlocal +exit /b 0 diff --git a/assets/banner.png b/assets/banner.png new file mode 100644 index 0000000..f7adf47 Binary files /dev/null and b/assets/banner.png differ diff --git a/assets/banner.svg b/assets/banner.svg new file mode 100644 index 0000000..d1f6e8b --- /dev/null +++ b/assets/banner.svg @@ -0,0 +1,59 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + WINDOWS FIREWALL / NO THIRD-PARTY TOOLS + + WinMasterBlocker + + Block Adobe, Autodesk, Corel and Maxon from the internet. + + + + + + > netsh advfirewall firewall add rule action=block + + + + github.com/ph33nx/WinMasterBlocker + + diff --git a/lefthook.yml b/lefthook.yml new file mode 100644 index 0000000..c905852 --- /dev/null +++ b/lefthook.yml @@ -0,0 +1,21 @@ +pre-commit: + parallel: true + commands: + lint-bat: + glob: "*.bat" + run: bash tools/lint-bat.sh {staged_files} + format-check: + glob: "*.bat" + run: bash tools/format-check.sh {staged_files} + audit-coverage: + glob: "WinMasterBlocker.bat" + run: bash tools/audit-coverage.sh + +pre-push: + commands: + audit-coverage: + run: bash tools/audit-coverage.sh + lint-bat: + run: bash tools/lint-bat.sh WinMasterBlocker.bat + format-check: + run: bash tools/format-check.sh WinMasterBlocker.bat diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..390fc86 --- /dev/null +++ b/llms.txt @@ -0,0 +1,51 @@ +# WinMasterBlocker + +> A Windows batch script that uses the built-in Windows Firewall to block Adobe, Autodesk, Corel, Maxon and Red Giant applications from reaching the internet. No drivers, no third-party tools, no subscription. MIT licensed. + +WinMasterBlocker creates `netsh advfirewall` rules per executable for the listed vendors. It is a single batch file with no dependencies. It targets Windows 10 and 11. + +## Quickstart + +Right-click `WinMasterBlocker.bat`, choose Run as administrator, then pick a vendor from the menu. The script walks every known install path for that vendor (`%ProgramFiles%\Adobe`, `%ProgramFiles(x86)%\Adobe`, `%CommonProgramFiles%\Adobe`, `%ProgramData%\Adobe`, `%LOCALAPPDATA%\Adobe`, `%APPDATA%\Adobe` for Adobe, similar for the other vendors) and adds inbound + outbound `block` rules for every `.exe` it finds. A transcript log lands at `%TEMP%\WinMasterBlocker-.log`. + +## What it blocks + +Adobe (including AcroCEF, RdrCEF, Acrobat, AcroRd32, AdobeNotificationClient, AdobeIPCBroker, Creative Cloud), Autodesk (AutoCAD, Maya, Revit licensing), Corel, Maxon (Cinema 4D), and Red Giant. The Adobe path covers a known-process sweep on top of the recursive walk so AcroCEF and RdrCEF are caught even when installed under non-standard paths. + +## Common questions + +**Is `acrocef.exe` blocked?** Yes, both by the recursive walk under `%ProgramFiles%\Adobe` and by an explicit known-process sweep. After an Adobe Acrobat update, re-run the script and pick option 98 to scan the new install state. + +**Does it block Windows Update or Defender?** No. The vendor list is Adobe, Corel, Autodesk, Maxon and Red Giant only. Nothing under `%WINDIR%` is walked. + +**How do I undo blocks?** Run the script and pick option 99. The script only removes rules whose name ends in `-block` so unrelated firewall rules are left alone. + +**Will Adobe still activate?** Yes, but you may need to temporarily disable the rules during initial activation. After activation completes, re-enable them. License re-activations after subscription changes work the same way. + +**Is it safe?** No drivers, no service installs, no scheduled tasks, no telemetry, no auto-update. Every action is logged before execution. The full source is one batch file you can read in five minutes. + +## Troubleshooting AcroCEF + +If `acrocef.exe` is still connecting after a run, three causes account for almost every report. First: Acrobat was updated after the script last ran. Fix: re-run the script (option 98). Second: Acrobat installed components under `%LOCALAPPDATA%\Adobe\` that the previous (pre-2.0) version of the script did not walk. Fix: upgrade to v2.0 which walks `%LOCALAPPDATA%` and `%APPDATA%`. Third (pre-2.0): the duplicate-check made one PowerShell call per executable, and large Adobe installs took 5+ minutes; users killed the run early. Fix: v2.0 caches existing rules once up front; the Adobe walk now completes in seconds. + +## Unattended mode + +```cmd +set WMB_VENDOR=Adobe +set WMB_ACTION=block +set WMB_QUIET=1 +WinMasterBlocker.bat +``` + +Set `WHATIF=1` to log every netsh call instead of executing it (dry run). + +## Repository + +- Source: https://github.com/ph33nx/WinMasterBlocker +- License: MIT (`SPDX-License-Identifier: MIT`) +- Citation: see `CITATION.cff` in the repo root +- Bug reports: https://github.com/ph33nx/WinMasterBlocker/issues with `%TEMP%\WinMasterBlocker-*.log` attached + +## Alternatives + +For users who want a GUI, packet logging or per-NIC shaping: SimpleWall (LGPL, free), NetLimiter (commercial), GlassWire (commercial). WinMasterBlocker is the right pick when you want native Windows Firewall rules only, no third-party drivers, and a script you can audit in one read. diff --git a/tools/audit-coverage.sh b/tools/audit-coverage.sh new file mode 100755 index 0000000..d910f81 --- /dev/null +++ b/tools/audit-coverage.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# Coverage audit: asserts that WinMasterBlocker.bat still covers the +# executables and paths we promise to cover. This is the regression +# guard against issues like #6 (AcroCEF leaking) being reintroduced by +# a future refactor. +# +# Add new entries to REQUIRED_EXES / REQUIRED_PATHS as new vendor or +# CEF children are discovered. + +set -u + +SCRIPT="${1:-WinMasterBlocker.bat}" +fail=0 + +err() { + printf '\033[31m[audit] %s\033[0m\n' "$1" >&2 + fail=1 +} + +ok() { + printf '[audit] ok: %s\n' "$1" +} + +if [[ ! -f "$SCRIPT" ]]; then + err "missing $SCRIPT" + exit 1 +fi + +# Adobe known-CEF coverage. These executables are explicitly named by the +# Adobe non-default-path sweep in WinMasterBlocker.bat. Regressing this +# list silently re-opens issue #6 (AcroCEF reaching the internet on +# custom installs), so we assert by literal substring match. +REQUIRED_ADOBE_EXES=( + "acrocef.exe" + "RdrCEF.exe" + "Acrobat.exe" + "AcroRd32.exe" + "AdobeNotificationClient.exe" + "AdobeIPCBroker.exe" + "AGSService.exe" + "AdobeUpdateService.exe" + "Creative Cloud.exe" +) + +for exe in "${REQUIRED_ADOBE_EXES[@]}"; do + if grep -qF "$exe" "$SCRIPT"; then + ok "Adobe known-exe present: $exe" + else + err "Adobe known-exe missing from script: $exe" + fi +done + +# Path coverage. These environment variables must appear in the Adobe +# paths string so non-C: and AppData installs are walked. The recursive +# *.exe walk under these paths is what catches the long tail of phone-home +# binaries; the .github/workflows/ci.yml integration job verifies the +# actual blocking behaviour with staged fake binaries. +REQUIRED_PATH_VARS=( + '%ProgramFiles%' + '%ProgramFiles(x86)%' + '%CommonProgramFiles%' + '%LOCALAPPDATA%' + '%APPDATA%' + '%ProgramData%' +) + +for v in "${REQUIRED_PATH_VARS[@]}"; do + if grep -qF "$v" "$SCRIPT"; then + ok "path var present: $v" + else + err "path var missing from script: $v" + fi +done + +# Behavioural affordances required by the test harness and by senior +# admin operating mode. +REQUIRED_TOKENS=( + "WHATIF" + "WMB_LOG" + "SPDX-License-Identifier" + "WMB_VERSION" +) + +for tok in "${REQUIRED_TOKENS[@]}"; do + if grep -qF "$tok" "$SCRIPT"; then + ok "token present: $tok" + else + err "token missing from script: $tok" + fi +done + +exit "$fail" diff --git a/tools/format-check.sh b/tools/format-check.sh new file mode 100755 index 0000000..673fc29 --- /dev/null +++ b/tools/format-check.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Format check for batch scripts: CRLF endings, no trailing whitespace, +# trailing newline, no tabs. +# +# Usage: tools/format-check.sh [ ...] + +set -u + +fail=0 + +err() { + printf '\033[31m[format] %s: %s\033[0m\n' "$1" "$2" >&2 + fail=1 +} + +ok() { + printf '[format] %s: %s\n' "$1" "$2" +} + +for f in "$@"; do + [[ "$f" == *.bat ]] || continue + [[ -f "$f" ]] || continue + + # CRLF check. We fail if any line is missing the trailing \r. + if grep -Uq $'[^\r]$' "$f"; then + err "$f" "lines missing CRLF endings (run: perl -pi -e 's/\\\\r?\\\\n/\\\\r\\\\n/g' $f)" + fi + + # Trailing whitespace before \r. + if grep -nE $'[ \t]+\r?$' "$f" >/dev/null; then + err "$f" "trailing whitespace on one or more lines" + fi + + # Tab characters anywhere. + if grep -nP '\t' "$f" >/dev/null; then + err "$f" "tab characters present (use spaces)" + fi + + # Final newline. + if [[ -n "$(tail -c 1 "$f")" ]]; then + err "$f" "no final newline" + fi + + [[ "$fail" -eq 0 ]] && ok "$f" "ok" +done + +exit "$fail" diff --git a/tools/lint-bat.sh b/tools/lint-bat.sh new file mode 100755 index 0000000..6f37092 --- /dev/null +++ b/tools/lint-bat.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# Lightweight linter for batch scripts. Catches the patterns that have +# historically broken WinMasterBlocker: missing admin guard, label drift, +# unbalanced setlocal/endlocal, BOM, unquoted netsh paths. +# +# Usage: tools/lint-bat.sh [ ...] + +set -u + +fail=0 + +err() { + printf '\033[31m[lint] %s: %s\033[0m\n' "$1" "$2" >&2 + fail=1 +} + +ok() { + printf '[lint] %s: %s\n' "$1" "$2" +} + +for f in "$@"; do + [[ "$f" == *.bat ]] || continue + [[ -f "$f" ]] || continue + + # 1. UTF-8 BOM at start of file. cmd.exe will print it as "´╗┐" on the + # first echoed line and breaks @echo off. + if head -c 3 "$f" | xxd -p | grep -q '^efbbbf'; then + err "$f" "UTF-8 BOM detected at start of file" + fi + + content=$(cat "$f") + + # 2. @echo off must appear within the first 30 lines. + if ! head -n 30 "$f" | grep -qiE '^[[:space:]]*@echo[[:space:]]+off'; then + err "$f" "missing @echo off in first 30 lines" + fi + + # 3. Admin elevation block. We require either net session check or + # a Start-Process RunAs invocation. + if ! grep -qiE 'net session|Start-Process .*RunAs' "$f"; then + err "$f" "missing admin elevation guard (net session or Start-Process RunAs)" + fi + + # 4. setlocal / endlocal balance. + setlocals=$(grep -ciE '^[[:space:]]*setlocal\b' "$f" || true) + endlocals=$(grep -ciE '^[[:space:]]*endlocal\b' "$f" || true) + if [[ "$setlocals" -gt 0 && "$endlocals" -lt 1 ]]; then + err "$f" "setlocal without endlocal (setlocal=$setlocals endlocal=$endlocals)" + fi + + # 5. Label drift: every :label (excluding :: comments, :eof / :EOF, and + # fall-through labels above the first goto/call) must be referenced + # by a goto or call somewhere in the file. + first_jump=$(grep -niE '^[[:space:]]*(goto|call)[[:space:]]+' "$f" \ + | head -1 | cut -d: -f1) + [[ -z "$first_jump" ]] && first_jump=0 + + labels_with_lines=$(grep -nE '^[[:space:]]*:[a-zA-Z_][a-zA-Z0-9_]*' "$f" \ + | tr -d '\r' || true) + + while IFS= read -r entry; do + [[ -z "$entry" ]] && continue + line_no=$(printf '%s\n' "$entry" | cut -d: -f1) + rest=$(printf '%s\n' "$entry" | cut -d: -f2-) + label=$(printf '%s' "$rest" | sed -E 's/^[[:space:]]*://' | awk '{print $1}') + case "$label" in + ""|eof|EOF|continue) continue ;; + esac + # Fall-through entries above the first goto/call are exempt. + if [[ "$line_no" -lt "$first_jump" ]]; then + continue + fi + if ! grep -qiE "(^|[[:space:]])(goto|call)[[:space:]]+:?${label}([[:space:]]|$)" "$f"; then + err "$f" "orphaned label :$label (no goto / call references)" + fi + done <<< "$labels_with_lines" + + # 6. netsh add rule without quoted program= path. netsh silently + # misparses paths with spaces and creates a rule against the wrong + # binary, which then never fires. + if grep -nE 'netsh advfirewall firewall add rule' "$f" \ + | grep -vE 'program="[^"]*"' \ + | grep -E 'program=' >/dev/null; then + err "$f" "netsh add rule with unquoted program= argument" + fi + + # 7. SPDX license identifier required. + if ! grep -q 'SPDX-License-Identifier:' "$f"; then + err "$f" "missing SPDX-License-Identifier header" + fi + + [[ "$fail" -eq 0 ]] && ok "$f" "ok" +done + +exit "$fail"