Skip to content

python edition #1

@mthcht

Description

@mthcht

#!/usr/bin/env python3
"""
VSXSentry - VS Code Extension Remove + Block Script
"""

import argparse
import json
import os
import shutil
import subprocess
import sys
from pathlib import Path

import requests
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)

DEFAULT_FEED_URL = "https://vsxsentry.github.io/feeds/ioc_all_extension_ids.txt"

def log(msg):
print(msg, flush=True)

def err(msg):
print(msg, file=sys.stderr, flush=True)

def run_command(cmd):
return subprocess.run(
cmd,
capture_output=True,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)

def find_code_cli():
log("[*] Searching for code CLI...")

code_bin = shutil.which("code")
if code_bin:
    log("[+] Found CLI: {}".format(code_bin))
    return code_bin

candidates = []
home = Path.home()

if sys.platform.startswith("win"):
    localappdata = os.environ.get("LOCALAPPDATA", "")
    programfiles = os.environ.get("ProgramFiles", "")
    programfiles_x86 = os.environ.get("ProgramFiles(x86)", "")

    if localappdata:
        candidates.append(Path(localappdata) / "Programs" / "Microsoft VS Code" / "bin" / "code.cmd")

    if programfiles:
        candidates.append(Path(programfiles) / "Microsoft VS Code" / "bin" / "code.cmd")
    if programfiles_x86:
        candidates.append(Path(programfiles_x86) / "Microsoft VS Code" / "bin" / "code.cmd")

    users_root = Path("C:/Users")
    if users_root.exists():
        for user_dir in users_root.iterdir():
            if not user_dir.is_dir():
                continue
            candidates.append(
                user_dir / "AppData" / "Local" / "Programs" / "Microsoft VS Code" / "bin" / "code.cmd"
            )

    if localappdata:
        candidates.append(
            Path(localappdata) / "Programs" / "Microsoft VS Code Insiders" / "bin" / "code-insiders.cmd"
        )
    if programfiles:
        candidates.append(
            Path(programfiles) / "Microsoft VS Code Insiders" / "bin" / "code-insiders.cmd"
        )
    if programfiles_x86:
        candidates.append(
            Path(programfiles_x86) / "Microsoft VS Code Insiders" / "bin" / "code-insiders.cmd"
        )
    if users_root.exists():
        for user_dir in users_root.iterdir():
            if not user_dir.is_dir():
                continue
            candidates.append(
                user_dir / "AppData" / "Local" / "Programs" / "Microsoft VS Code Insiders" / "bin" / "code-insiders.cmd"
            )

elif sys.platform == "darwin":
    candidates.extend([
        Path("/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"),
        home / "Applications" / "Visual Studio Code.app/Contents/Resources/app/bin/code",
    ])
else:
    candidates.extend([
        Path("/usr/bin/code"),
        Path("/usr/share/code/bin/code"),
        Path("/snap/bin/code"),
        home / ".local/bin/code",
    ])

seen = set()
for candidate in candidates:
    candidate_str = str(candidate).lower()
    if candidate_str in seen:
        continue
    seen.add(candidate_str)

    if candidate.is_file():
        log("[+] Found CLI: {}".format(candidate))
        return str(candidate)

err("[!] ERROR: Could not find code CLI for the current context or any local user profile.")
err("    Searched PATH, common install locations, and Windows user profiles.")
err("    Install VS Code system-wide or provide --code-bin explicitly.")
return None

def fetch_text(url, timeout=30):
response = requests.get(
url,
headers={
"User-Agent": "VSXSentry/1.0",
"Accept": "text/plain, /",
},
timeout=timeout,
verify=False,
)
response.raise_for_status()
return response.text

def load_feed(feed_url):
log("[*] Fetching feed from {} ...".format(feed_url))

try:
    raw = fetch_text(feed_url, timeout=30)
except requests.exceptions.HTTPError as e:
    err("[!] ERROR: Feed request failed with HTTP {}".format(e.response.status_code if e.response else "?"))
    raise SystemExit(1)
except requests.exceptions.ConnectionError as e:
    err("[!] ERROR: Could not reach feed URL ({})".format(e))
    raise SystemExit(1)
except requests.exceptions.Timeout:
    err("[!] ERROR: Feed request timed out")
    raise SystemExit(1)
except Exception as e:
    err("[!] ERROR: Failed to fetch feed ({})".format(e))
    raise SystemExit(1)

feed_ids = set()
for line in raw.splitlines():
    line = line.strip().lower()
    if line:
        feed_ids.add(line)

if not feed_ids:
    err("[!] ERROR: Feed returned 0 IDs.")
    raise SystemExit(1)

log("[+] Loaded {} extension IDs from remote feed".format(len(feed_ids)))
return feed_ids

def build_extensions_allowed_policy(feed_ids):
policy = {"*": True}
for ext_id in sorted(feed_ids):
policy[ext_id] = False
return policy

def iter_profile_settings_files(user_dir):
profiles_dir = user_dir / "profiles"
if not profiles_dir.is_dir():
return

for profile_dir in profiles_dir.iterdir():
    if profile_dir.is_dir():
        yield profile_dir / "settings.json"

def collect_settings_targets():
targets = []
seen = set()

def add_target(path_obj):
    path_str = str(path_obj).lower()
    if path_str in seen:
        return
    seen.add(path_str)
    targets.append(path_obj)

if sys.platform.startswith("win"):
    appdata = os.environ.get("APPDATA", "")
    if appdata:
        for product in ("Code", "Code - Insiders"):
            user_dir = Path(appdata) / product / "User"
            add_target(user_dir / "settings.json")
            for profile_settings in iter_profile_settings_files(user_dir):
                add_target(profile_settings)

    users_root = Path("C:/Users")
    if users_root.exists():
        for user_dir in users_root.iterdir():
            if not user_dir.is_dir():
                continue
            roaming = user_dir / "AppData" / "Roaming"
            for product in ("Code", "Code - Insiders"):
                vscode_user_dir = roaming / product / "User"
                add_target(vscode_user_dir / "settings.json")
                for profile_settings in iter_profile_settings_files(vscode_user_dir):
                    add_target(profile_settings)

elif sys.platform == "darwin":
    homes = [Path.home(), Path("/var/root")]
    users_root = Path("/Users")
    if users_root.exists():
        for user_dir in users_root.iterdir():
            if user_dir.is_dir():
                homes.append(user_dir)

    for home_dir in homes:
        for product in ("Code", "Code - Insiders"):
            vscode_user_dir = home_dir / "Library" / "Application Support" / product / "User"
            add_target(vscode_user_dir / "settings.json")
            for profile_settings in iter_profile_settings_files(vscode_user_dir):
                add_target(profile_settings)

else:
    homes = [Path.home(), Path("/root")]
    users_root = Path("/home")
    if users_root.exists():
        for user_dir in users_root.iterdir():
            if user_dir.is_dir():
                homes.append(user_dir)

    for home_dir in homes:
        for product in ("Code", "Code - Insiders"):
            vscode_user_dir = home_dir / ".config" / product / "User"
            add_target(vscode_user_dir / "settings.json")
            for profile_settings in iter_profile_settings_files(vscode_user_dir):
                add_target(profile_settings)

return targets

def atomic_write_json(path_obj, data):
path_obj.parent.mkdir(parents=True, exist_ok=True)

tmp_path = str(path_obj) + ".vsxsentry.tmp.{}".format(os.getpid())

try:
    with open(tmp_path, "w", encoding="utf-8") as f:
        json.dump(data, f, indent=2, sort_keys=True)
        f.write("\n")
    os.replace(tmp_path, str(path_obj))
finally:
    try:
        if os.path.exists(tmp_path):
            os.remove(tmp_path)
    except Exception:
        pass

def apply_block_policy(feed_ids):
policy = build_extensions_allowed_policy(feed_ids)
targets = collect_settings_targets()

if not targets:
    log("[*] No VS Code settings targets found for block policy.")
    return 0, 0

applied = 0
failed = 0

for settings_path in targets:
    try:
        settings_path.parent.mkdir(parents=True, exist_ok=True)

        current = {}
        if settings_path.exists():
            backup_path = Path(str(settings_path) + ".vsxsentry.bak")
            shutil.copy2(str(settings_path), str(backup_path))

            try:
                with open(str(settings_path), "r", encoding="utf-8") as f:
                    loaded = json.load(f)
                if isinstance(loaded, dict):
                    current = loaded
            except Exception:
                invalid_backup_path = Path(str(settings_path) + ".vsxsentry.invalid.bak")
                shutil.copy2(str(settings_path), str(invalid_backup_path))
                current = {}

        current["extensions.allowed"] = policy
        atomic_write_json(settings_path, current)

        log("[+] Block policy written to {}".format(settings_path))
        applied += 1

    except Exception as e:
        err("[!] Failed to write block policy to {} ({})".format(settings_path, e))
        failed += 1

return applied, failed

def list_installed_extensions(code_bin):
log("[*] Listing installed extensions...")

result = run_command([code_bin, "--list-extensions"])

if result.returncode != 0:
    err("[!] ERROR: Failed to list extensions.")
    err("    Tried: {} --list-extensions".format(code_bin))
    if result.stderr.strip():
        err("    STDERR: {}".format(result.stderr.strip()))
    raise SystemExit(1)

installed = []
for line in result.stdout.splitlines():
    line = line.strip()
    if line:
        installed.append(line)

log("[+] Found {} installed extensions".format(len(installed)))
return installed

def prompt_confirm(count, auto_yes):
if auto_yes:
return True

try:
    answer = input("Proceed with removal of {} extension(s)? (y/N) ".format(count)).strip().lower()
except EOFError:
    return False

return answer == "y"

def uninstall_extensions(code_bin, matches):
removed = 0
failed = 0

for ext in matches:
    log("[*] Removing {} ...".format(ext))
    result = run_command([code_bin, "--uninstall-extension", ext])

    if result.returncode == 0:
        log("    [OK] Removed {}".format(ext))
        removed += 1
    else:
        msg = result.stderr.strip() or result.stdout.strip() or "exit code {}".format(result.returncode)
        err("    [FAIL] Could not remove {} ({})".format(ext, msg))
        failed += 1

return removed, failed

def parse_args():
parser = argparse.ArgumentParser(
description="Find, block, and remove installed VS Code extensions matching the remote VSXSentry feed.",
allow_abbrev=False,
)
parser.add_argument("--yes", action="store_true")
parser.add_argument("--dry-run", action="store_true")
parser.add_argument("--code-bin")
parser.add_argument("--feed-url", default=DEFAULT_FEED_URL)
parser.add_argument("--no-block-policy", action="store_true")
parser.add_argument("--block-only", action="store_true")
args, _unknown = parser.parse_known_args()
return args

def main():
args = parse_args()
feed_ids = load_feed(args.feed_url)

if args.dry_run:
    code_bin = args.code_bin or find_code_cli()
    if not code_bin:
        err("[!] ERROR: Could not find code CLI for dry-run removal check.")
        return 1

    installed = list_installed_extensions(code_bin)
    installed_by_lower = {}
    for ext in installed:
        installed_by_lower[ext.lower()] = ext

    matches = sorted(installed_by_lower[ext_id] for ext_id in feed_ids if ext_id in installed_by_lower)

    if not matches:
        log("[+] No matching extensions found - all clean!")
    else:
        log("")
        log("[!] ALERT: {} matching extension(s) found:".format(len(matches)))
        for ext in matches:
            log("    - {}".format(ext))
        log("")

    log("[+] Dry run mode enabled. No changes were made.")
    return 0

block_applied = 0
block_failed = 0

if not args.no_block_policy:
    log("[*] Applying extensions.allowed block policy...")
    block_applied, block_failed = apply_block_policy(feed_ids)
    log("[+] Block policy applied to {} settings file(s)".format(block_applied))
    if block_failed:
        err("[!] Block policy failed on {} settings file(s)".format(block_failed))

if args.block_only:
    return 1 if block_failed > 0 else 0

code_bin = args.code_bin or find_code_cli()
if not code_bin:
    if block_applied or block_failed:
        err("[!] WARNING: Block policy step completed, but removal step was skipped because the VS Code CLI was not found.")
    return 1

installed = list_installed_extensions(code_bin)

if not installed:
    log("[+] No extensions installed. Nothing to remove.")
    return 1 if block_failed > 0 else 0

installed_by_lower = {}
for ext in installed:
    installed_by_lower[ext.lower()] = ext

matches = sorted(installed_by_lower[ext_id] for ext_id in feed_ids if ext_id in installed_by_lower)

if not matches:
    log("[+] No matching extensions found - all clean!")
    return 1 if block_failed > 0 else 0

log("")
log("[!] ALERT: {} matching extension(s) found:".format(len(matches)))
for ext in matches:
    log("    - {}".format(ext))
log("")

if not prompt_confirm(len(matches), args.yes):
    log("Aborted by user.")
    return 0

removed, failed = uninstall_extensions(code_bin, matches)

log("")
log("[+] Done: {} removed, {} failed out of {} total".format(removed, failed, len(matches)))

total_failures = failed + block_failed
return 1 if total_failures > 0 else 0

if name == "main":
sys.exit(main())

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions