#!/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())
#!/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...")
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))
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
def collect_settings_targets():
targets = []
seen = set()
def atomic_write_json(path_obj, data):
path_obj.parent.mkdir(parents=True, exist_ok=True)
def apply_block_policy(feed_ids):
policy = build_extensions_allowed_policy(feed_ids)
targets = collect_settings_targets()
def list_installed_extensions(code_bin):
log("[*] Listing installed extensions...")
def prompt_confirm(count, auto_yes):
if auto_yes:
return True
def uninstall_extensions(code_bin, matches):
removed = 0
failed = 0
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 name == "main":
sys.exit(main())