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

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions