|
#!/usr/bin/env python3 |
|
""" |
|
list installed VS Code extensions with local install date, marketplace last-updated. |
|
Flags extensions whose marketplace update is recent (potential supply-chain risk). |
|
""" |
|
|
|
import json |
|
import subprocess |
|
import time |
|
from datetime import datetime, UTC |
|
from pathlib import Path |
|
from urllib import error, request |
|
|
|
EXTENSIONS_DIR = Path.home() / ".vscode" / "extensions" |
|
SCRIPT_DIR = Path(__file__).parent |
|
RESTORE_SCRIPT = SCRIPT_DIR / "install-extensions.sh" |
|
MD_REPORT = SCRIPT_DIR / "vscode-extensions.md" |
|
MARKET_DAYS = 30 |
|
BATCH_SIZE = 50 |
|
|
|
BOLD = "\033[1m" |
|
RESET = "\033[0m" |
|
YELLOW = "\033[33m" |
|
GREEN = "\033[32m" |
|
DIM = "\033[2m" |
|
|
|
|
|
def collect_extensions(): |
|
raw = subprocess.check_output(["code", "--list-extensions", "--show-versions"], text=True) |
|
extensions = [] |
|
for line in raw.strip().splitlines(): |
|
line = line.strip() |
|
if "@" not in line: |
|
continue |
|
ext_id, _, version = line.rpartition("@") |
|
ext_id = ext_id.lower() |
|
|
|
local_epoch = 0 |
|
try: |
|
matches = [p for p in EXTENSIONS_DIR.iterdir() if p.name.lower().startswith(ext_id + "-")] |
|
if matches: |
|
local_epoch = int(max(p.stat().st_mtime for p in matches)) |
|
except OSError: |
|
pass |
|
|
|
extensions.append((ext_id, version, local_epoch)) |
|
return extensions |
|
|
|
|
|
def parse_iso(s): |
|
if not s: |
|
return 0 |
|
s = s.rstrip("Z") |
|
if "." in s: |
|
s = s[:s.index(".") + 7] |
|
try: |
|
return int(datetime.fromisoformat(s).replace(tzinfo=UTC).timestamp()) |
|
except ValueError: |
|
return 0 |
|
|
|
|
|
def query_marketplace(ext_ids): |
|
criteria = [{"filterType": 7, "value": i} for i in ext_ids] |
|
payload = json.dumps({"filters": [{"criteria": criteria}], "flags": 512}).encode() |
|
req = request.Request( |
|
"https://marketplace.visualstudio.com/_apis/public/gallery/extensionquery", |
|
data=payload, |
|
headers={ |
|
"Content-Type": "application/json", |
|
"Accept": "application/json;api-version=3.0-preview.1", |
|
}, |
|
) |
|
with request.urlopen(req, timeout=30) as resp: |
|
return json.load(resp) |
|
|
|
|
|
def fetch_marketplace_info(ext_ids): |
|
info = {} |
|
for i in range(0, len(ext_ids), BATCH_SIZE): |
|
batch = ext_ids[i:i + BATCH_SIZE] |
|
try: |
|
data = query_marketplace(batch) |
|
for ext in data.get("results", [{}])[0].get("extensions", []): |
|
pub = ext.get("publisher", {}).get("publisherName", "") |
|
name = ext.get("extensionName", "") |
|
eid = f"{pub}.{name}".lower() |
|
versions = ext.get("versions", []) |
|
info[eid] = { |
|
"last_updated": parse_iso(ext.get("lastUpdated", "")), |
|
"latest_version": versions[0].get("version", "?") if versions else "?", |
|
} |
|
except error.URLError as e: |
|
print(f" [warning] marketplace query failed: {e}", flush=True) |
|
return info |
|
|
|
|
|
def fmt_epoch(epoch): |
|
return datetime.fromtimestamp(epoch).strftime("%Y-%m-%d %H:%M") if epoch else "not found" |
|
|
|
|
|
def write_restore_script(extensions): |
|
lines = [ |
|
"#!/usr/bin/env bash", |
|
f"# auto-generated by audit.py on {datetime.now().strftime('%Y-%m-%d')}", |
|
"# installs all vscode extensions at their currently-installed versions", |
|
"", |
|
] |
|
for ext_id, version, _ in extensions: |
|
lines.append(f"code --install-extension {ext_id}@{version}") |
|
RESTORE_SCRIPT.write_text("\n".join(lines) + "\n") |
|
RESTORE_SCRIPT.chmod(0o755) |
|
|
|
|
|
def write_md_report(rows, total): |
|
now_epoch = int(time.time()) |
|
recent_cutoff = now_epoch - MARKET_DAYS * 86400 |
|
|
|
def fmt_md(epoch): |
|
s = fmt_epoch(epoch) |
|
return f"{s} ⚠️" if epoch > recent_cutoff else s |
|
|
|
lines = [ |
|
"# VS Code Extensions", |
|
"", |
|
f"_generated {datetime.now().strftime('%Y-%m-%d %H:%M')} — {total} extensions total_", |
|
"", |
|
] |
|
md_header = "| Extension | Installed | Latest | Local Install Date | Marketplace Updated |" |
|
md_sep = "| --- | --- | --- | --- | --- |" |
|
for flag_label, _, sort_key, _ in GROUPS: |
|
group = sorted([r for r in rows if r["flag"] == flag_label], key=sort_key) |
|
if not group: |
|
continue |
|
lines += [f"## {MD_GROUP_HEADINGS[flag_label]}", "", md_header, md_sep] |
|
for r in group: |
|
lines.append( |
|
f"| {r['ext_id']} | {r['inst_ver']} | {r['latest_ver']} " |
|
f"| {fmt_md(r['local_epoch'])} | {fmt_md(r['mkt_epoch'])} |" |
|
) |
|
lines.append("") |
|
MD_REPORT.write_text("\n".join(lines)) |
|
|
|
|
|
MD_GROUP_HEADINGS = { |
|
"⚠ recently updated": "⚠ Recently Updated (potential supply-chain risk)", |
|
"↑ update available": "↑ Update Available", |
|
"": "Current", |
|
} |
|
|
|
GROUPS = [ |
|
# (flag_label, color, sort_key, print_to_stdout) -- order here controls print order |
|
("⚠ recently updated", YELLOW, lambda r: (-r["mkt_epoch"], -r["local_epoch"]), True), |
|
("↑ update available", DIM, lambda r: (-r["local_epoch"], -r["mkt_epoch"]), False), |
|
("", "", lambda r: (-r["local_epoch"], -r["mkt_epoch"]), False), |
|
] |
|
|
|
|
|
def main(): |
|
extensions = collect_extensions() |
|
print(f"Found {len(extensions)} installed extensions. Querying marketplace...", flush=True) |
|
|
|
mkt = fetch_marketplace_info([e[0] for e in extensions]) |
|
|
|
now_epoch = int(time.time()) |
|
recent_cutoff = now_epoch - MARKET_DAYS * 86400 |
|
|
|
# build row dicts |
|
rows = [] |
|
for ext_id, inst_ver, local_epoch in extensions: |
|
info = mkt.get(ext_id, {}) |
|
mkt_epoch = info.get("last_updated", 0) |
|
latest_ver = info.get("latest_version", "?") |
|
|
|
if mkt_epoch > recent_cutoff and mkt_epoch > local_epoch: |
|
flag = "⚠ recently updated" |
|
elif latest_ver != "?" and inst_ver != latest_ver: |
|
flag = "↑ update available" |
|
else: |
|
flag = "" |
|
|
|
rows.append({ |
|
"ext_id": ext_id, |
|
"inst_ver": inst_ver, |
|
"latest_ver": latest_ver, |
|
"local_epoch": local_epoch, |
|
"mkt_epoch": mkt_epoch, |
|
"flag": flag, |
|
}) |
|
|
|
col = [50, 12, 12, 22, 22] |
|
header = ( |
|
f"{'EXTENSION':<{col[0]}} {'INSTALLED':<{col[1]}} {'LATEST':<{col[2]}} " |
|
f"{'LOCAL INSTALL DATE':<{col[3]}} {'MARKETPLACE UPDATED':<{col[4]}} FLAGS" |
|
) |
|
|
|
for flag_label, color, sort_key, print_to_stdout in GROUPS: |
|
group = sorted([r for r in rows if r["flag"] == flag_label], key=sort_key) |
|
if not group: |
|
continue |
|
if not print_to_stdout: |
|
continue |
|
print(f"\n{BOLD}{header}{RESET}") |
|
print("-" * 140) |
|
for r in group: |
|
print( |
|
f"{color}{r['ext_id']:<{col[0]}} {r['inst_ver']:<{col[1]}} {r['latest_ver']:<{col[2]}} " |
|
f"{fmt_epoch(r['local_epoch']):<{col[3]}} {fmt_epoch(r['mkt_epoch']):<{col[4]}} {r['flag']}{RESET}" |
|
) |
|
|
|
flagged = [r["ext_id"] for r in rows if r["flag"] == "⚠ recently updated"] |
|
updates = [r for r in rows if r["flag"] == "↑ update available"] |
|
safe_updates = [r["ext_id"] for r in updates if r["mkt_epoch"] <= recent_cutoff] |
|
|
|
print(f"\n{BOLD}Summary:{RESET}") |
|
print(f" Total extensions : {len(extensions)}") |
|
if flagged: |
|
print(f" {YELLOW}{BOLD}Recently updated (⚠) : {len(flagged)}{RESET}") |
|
for eid in flagged: |
|
print(f" - {eid}") |
|
else: |
|
print(f" {GREEN}No extensions flagged as recently updated.{RESET}") |
|
print(f" Updates available (↑) : {len(updates)}") |
|
print(f" Updates older than {MARKET_DAYS}d : {len(safe_updates)}") |
|
|
|
write_restore_script(extensions) |
|
write_md_report(rows, len(extensions)) |
|
print(f"\nRestore script written to: {RESTORE_SCRIPT}") |
|
print(f"Markdown report written to: {MD_REPORT}") |
|
|
|
|
|
if __name__ == "__main__": |
|
main() |