> ## Documentation Index
> Fetch the complete documentation index at: https://docs.endorlabs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Block malicious VS Code extensions with Package Firewall

> Block malicious VS Code extension installations from the Microsoft Marketplace with Package Firewall, deployed through your MDM tool.

export const authTarget_0 = "the deployment scripts"

export const LicenseBadge = ({sku, skus, relation = 'any'}) => {
  const DATA_URL = '/snippets/license-sku-data.json';
  const CACHE_KEY = 'license-sku-data';
  const CACHE_TTL_MS = 60 * 60 * 1000;
  const REGISTRY_KEY = '__licenseSkuRegistry';
  const FALLBACK_LICENSES_URL = '/introduction/licenses';
  const ACCENT = '#26D07C';
  const CONJUNCTION = {
    any: 'or',
    all: 'and'
  };
  const FONT_STACK = '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
  const [isDark, setIsDark] = useState(false);
  const [data, setData] = useState(null);
  const [hasFetchError, setHasFetchError] = useState(false);
  const skuList = useMemo(() => {
    if (Array.isArray(skus) && skus.length > 0) {
      return skus.filter(code => typeof code === 'string' && code.trim()).map(c => c.trim());
    }
    if (typeof sku === 'string' && sku.trim()) {
      return [sku.trim()];
    }
    return [];
  }, [sku, skus]);
  useEffect(() => {
    const check = () => {
      const root = document.documentElement;
      setIsDark(root.dataset.theme === 'dark' || root.classList.contains('dark'));
    };
    check();
    const observer = new MutationObserver(check);
    observer.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => observer.disconnect();
  }, []);
  useEffect(() => {
    let cancelled = false;
    const readCache = () => {
      try {
        const raw = sessionStorage.getItem(CACHE_KEY);
        if (!raw) return null;
        const parsed = JSON.parse(raw);
        if (Date.now() - parsed.ts > CACHE_TTL_MS) {
          sessionStorage.removeItem(CACHE_KEY);
          return null;
        }
        return parsed.data;
      } catch (e) {
        return null;
      }
    };
    const writeCache = value => {
      try {
        sessionStorage.setItem(CACHE_KEY, JSON.stringify({
          ts: Date.now(),
          data: value
        }));
      } catch (e) {}
    };
    const fetchSkuData = async () => {
      const cached = readCache();
      if (cached) return cached;
      if (!globalThis[REGISTRY_KEY]) {
        globalThis[REGISTRY_KEY] = {};
      }
      const registry = globalThis[REGISTRY_KEY];
      if (registry[CACHE_KEY]) return registry[CACHE_KEY];
      const promise = (async () => {
        const resp = await fetch(DATA_URL);
        if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
        const json = await resp.json();
        writeCache(json);
        return json;
      })();
      registry[CACHE_KEY] = promise;
      promise.finally(() => {
        delete registry[CACHE_KEY];
      });
      return promise;
    };
    fetchSkuData().then(d => {
      if (!cancelled) setData(d);
    }).catch(() => {
      if (!cancelled) setHasFetchError(true);
    });
    return () => {
      cancelled = true;
    };
  }, []);
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const textMuted = isDark ? 'rgba(230,237,243,0.65)' : 'rgba(31,41,55,0.65)';
  const bannerBackground = isDark ? '#161b22' : '#f6f8fa';
  const borderColor = isDark ? 'rgba(38,208,124,0.35)' : 'rgba(38,208,124,0.45)';
  const linkColor = isDark ? '#4ade80' : '#047857';
  const errorBackground = isDark ? '#3b1111' : '#fef2f2';
  const errorBorder = isDark ? '#7f1d1d' : '#fecaca';
  const errorText = isDark ? '#fecaca' : '#7f1d1d';
  const licensesUrl = data?.licensesPageUrl || FALLBACK_LICENSES_URL;
  const resolveSkuName = code => {
    const entry = data?.skus?.[code];
    if (entry?.name) return entry.name;
    return null;
  };
  const formatSkuLabel = code => {
    const name = resolveSkuName(code);
    if (name) return name;
    return code;
  };
  const joinWithConjunction = (codes, conjunction) => {
    const labels = codes.map(formatSkuLabel);
    if (labels.length === 0) return '';
    if (labels.length === 1) return labels[0];
    if (labels.length === 2) return `${labels[0]} ${conjunction} ${labels[1]}`;
    const head = labels.slice(0, -1).join(', ');
    const tail = labels[labels.length - 1];
    return `${head}, ${conjunction} ${tail}`;
  };
  const buildSkuSentence = codes => {
    const conj = CONJUNCTION[relation] || CONJUNCTION.any;
    const names = joinWithConjunction(codes, conj);
    const noun = codes.length > 1 ? 'licenses' : 'license';
    return `${names} ${noun}`;
  };
  const renderLink = text => <a href={licensesUrl} className="lic-link" style={{
    color: linkColor,
    fontSize: '0.75rem',
    fontWeight: 500,
    textDecoration: 'none',
    whiteSpace: 'nowrap'
  }}>
      {text} →
    </a>;
  const renderBanner = codes => <div className="lic-banner not-prose" style={{
    margin: '1rem 0',
    padding: '0.5rem 0.85rem',
    background: bannerBackground,
    border: `1px solid ${borderColor}`,
    borderLeft: `3px solid ${ACCENT}`,
    borderRadius: '6px',
    color: textColor,
    fontSize: '0.75rem',
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    flexWrap: 'wrap',
    fontFamily: FONT_STACK,
    lineHeight: 1.5
  }}>
      <span style={{
    flex: 1,
    minWidth: 0
  }}>
        <span style={{
    color: textMuted,
    marginRight: '0.3rem'
  }}>Requires</span>
        <span style={{
    fontWeight: 600
  }}>{buildSkuSentence(codes)}</span>
      </span>
      {renderLink('Licenses')}
    </div>;
  const renderLoading = () => <div className="not-prose" style={{
    margin: '1rem 0',
    padding: '0.6rem 0.9rem',
    background: bannerBackground,
    border: `1px dashed ${borderColor}`,
    borderRadius: '6px',
    color: textMuted,
    fontSize: '0.85rem',
    fontStyle: 'italic',
    fontFamily: FONT_STACK
  }} role="status" aria-live="polite">
      Loading license info…
    </div>;
  const renderInputError = message => <div className="not-prose" style={{
    margin: '1rem 0',
    padding: '0.6rem 0.9rem',
    background: errorBackground,
    border: `1px solid ${errorBorder}`,
    borderRadius: '6px',
    color: errorText,
    fontSize: '0.85rem',
    fontFamily: FONT_STACK
  }} role="alert">
      {message}
    </div>;
  if (typeof sku === 'string' && Array.isArray(skus)) {
    return renderInputError('LicenseBadge: pass either `sku` or `skus`, not both.');
  }
  if (skuList.length === 0) {
    return renderInputError('LicenseBadge: `sku` or `skus` is required.');
  }
  if (hasFetchError) return renderBanner(skuList);
  if (!data) return renderLoading();
  return renderBanner(skuList);
};

export const PackageFirewallMdmGenerator = ({owner = 'endorlabs', repo = 'mdm-scripts', branch = 'main', basePath = 'package-firewall', lockedEcosystem = ''}) => {
  const CACHE_TTL = 60 * 60 * 1000;
  const COPY_FEEDBACK_MS = 2000;
  const COPY_LABEL = 'Copy';
  const OUTPUT_COLLAPSED_MAX_HEIGHT = '320px';
  const TEXTAREA_MIN_HEIGHT = '120px';
  const rawUrl = rel => 'https://raw.githubusercontent.com/' + owner + '/' + repo + '/' + branch + '/' + basePath + '/' + rel;
  const FILES = {
    bash: {
      generate: 'bash/generate.sh',
      common: 'bash/lib/common.sh',
      envsh: 'bash/templates/envsh.sh',
      js: 'bash/templates/js.sh',
      python: 'bash/templates/python.sh',
      go: 'bash/templates/go.sh',
      maven: 'bash/templates/maven.sh',
      vscode: 'bash/templates/vscode.sh',
      remove: 'bash/templates/remove.sh',
      bENVSH: 'shared/blocks/envsh.txt',
      bNPMRC: 'shared/blocks/npmrc.txt',
      bYARNC: 'shared/blocks/yarnrc_classic.txt',
      bYARN: 'shared/blocks/yarnrc.txt',
      bPIP: 'shared/blocks/pipconf.txt',
      bUV: 'shared/blocks/uvtoml.txt',
      bGO: 'shared/blocks/goenv.txt',
      bMAVEN: 'shared/blocks/mavensettings.txt'
    },
    powershell: {
      generate: 'powershell/generate.ps1',
      common: 'powershell/lib/common.ps1',
      header: 'powershell/templates/script-header.ps1',
      envvars: 'powershell/templates/envvars.ps1',
      js: 'powershell/templates/js.ps1',
      python: 'powershell/templates/python.ps1',
      go: 'powershell/templates/go.ps1',
      maven: 'powershell/templates/maven.ps1',
      vscode: 'powershell/templates/vscode.ps1',
      remove: 'powershell/templates/remove.ps1',
      bNPMRC: 'shared/blocks/npmrc.txt',
      bYARNC: 'shared/blocks/yarnrc_classic.txt',
      bYARN: 'shared/blocks/yarnrc.txt',
      bPIP: 'shared/blocks/pipconf.txt',
      bUV: 'shared/blocks/uvtoml.txt',
      bGO: 'shared/blocks/goenv.txt',
      bMAVEN: 'shared/blocks/mavensettings.txt'
    }
  };
  const PINS = {
    bash: {
      generate: 'b2e4fa3ee3e05b3a6a58007a98cbe35a94ad037a',
      common: '5dab948835ba7a48d446b40afb94665e09172db7'
    },
    powershell: {
      generate: 'd7953aeb1f5fd621f10db3a6b972ffde26de7738'
    }
  };
  const SHELLS = {
    bash: {
      label: 'Bash (macOS / Linux)',
      ext: 'sh',
      runAs: 'root'
    },
    powershell: {
      label: 'PowerShell (Windows)',
      ext: 'ps1',
      runAs: 'SYSTEM'
    }
  };
  const ECOSYSTEMS = [{
    value: 'all',
    label: 'All ecosystems'
  }, {
    value: 'js',
    label: 'JavaScript (npm, pnpm, yarn, bun)'
  }, {
    value: 'python',
    label: 'Python (pip, uv, poetry)'
  }, {
    value: 'go',
    label: 'Go (go modules)'
  }, {
    value: 'maven',
    label: 'Maven (settings.xml)'
  }, {
    value: 'vscode',
    label: 'VS Code (extension gallery)'
  }, {
    value: 'remove',
    label: 'Remove (offboarding)'
  }];
  const BLOCK_META = {
    bENVSH: {
      label: 'Credentials block (env.sh)',
      shells: ['bash'],
      ecos: ['js', 'python', 'go', 'all']
    },
    bNPMRC: {
      label: 'npm / pnpm / yarn classic / bun (.npmrc)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bYARNC: {
      label: 'yarn classic (.yarnrc)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bYARN: {
      label: 'yarn 2+ berry (.yarnrc.yml)',
      shells: ['bash', 'powershell'],
      ecos: ['js', 'all']
    },
    bPIP: {
      label: 'pip (pip.conf / pip.ini)',
      shells: ['bash', 'powershell'],
      ecos: ['python', 'all']
    },
    bUV: {
      label: 'uv (uv.toml)',
      shells: ['bash', 'powershell'],
      ecos: ['python', 'all']
    },
    bGO: {
      label: 'Go (go env, GOPROXY)',
      shells: ['bash', 'powershell'],
      ecos: ['go', 'all']
    },
    bMAVEN: {
      label: 'Maven (~/.m2/settings.xml)',
      shells: ['bash', 'powershell'],
      ecos: ['maven', 'all']
    }
  };
  const MDM = {
    bash: {
      kandji: {
        label: 'Kandji',
        steps: ['Library → Custom Scripts → Add Script.', 'Paste the script content or upload the file.', 'Set Run as: Root.', 'Set Execution Frequency: Run once per device (or every check-in for ongoing enforcement).', 'Assign to the relevant device blueprint.']
      },
      jamf: {
        label: 'Jamf Pro',
        steps: ['Settings → Scripts → New. Paste the script content.', 'Policies → New Policy → Scripts. Add your script.', 'Set Execution Frequency as appropriate.', 'Scope to the target devices.']
      },
      generic: {
        label: 'Generic MDM',
        steps: ['Upload the script file.', 'Ensure it runs as root: the script detects the logged-in console user and writes config to the correct home directory.']
      }
    },
    powershell: {
      intune: {
        label: 'Microsoft Intune',
        steps: ['Devices → Scripts and remediations → Platform scripts → Add.', 'Upload the .ps1 file.', 'Run this script using the logged on credentials: No (runs as SYSTEM).', 'Enforce script signature check: No.', 'Run script in 64-bit PowerShell: Yes.', 'Assign to the target device group.']
      },
      generic: {
        label: 'Generic MDM',
        steps: ['Upload the script file.', 'Ensure it runs as SYSTEM: the script detects the logged-in console user via explorer.exe and writes to the correct user profile.']
      }
    }
  };
  const DEFAULT_TOOL = {
    bash: 'kandji',
    powershell: 'intune'
  };
  const [isDark, setIsDark] = useState(false);
  const [shell, setShell] = useState('bash');
  const [ecosystem, setEcosystem] = useState(lockedEcosystem || 'all');
  const [mdmTool, setMdmTool] = useState('kandji');
  const [ns, setNs] = useState('');
  const [keyId, setKeyId] = useState('');
  const [secret, setSecret] = useState('');
  const [showSecret, setShowSecret] = useState(false);
  const [showCustomize, setShowCustomize] = useState(false);
  const [blockEdits, setBlockEdits] = useState({});
  const [sources, setSources] = useState({});
  const [status, setStatus] = useState('loading');
  const [copyLabel, setCopyLabel] = useState(COPY_LABEL);
  const copyTimer = useRef(null);
  const [expanded, setExpanded] = useState(false);
  const [drift, setDrift] = useState(null);
  useEffect(() => {
    const check = () => {
      const r = document.documentElement;
      setIsDark(r.dataset.theme === 'dark' || r.classList.contains('dark'));
    };
    check();
    const obs = new MutationObserver(check);
    obs.observe(document.documentElement, {
      attributes: true,
      attributeFilter: ['data-theme', 'class']
    });
    return () => obs.disconnect();
  }, []);
  useEffect(() => () => clearTimeout(copyTimer.current), []);
  useEffect(() => {
    if (sources[shell]) {
      setDrift(null);
      setStatus('ready');
      return undefined;
    }
    let cancelled = false;
    const gitBlobSha1 = async text => {
      const enc = new TextEncoder();
      const body = enc.encode(text);
      const head = enc.encode('blob ' + body.length + '\u0000');
      const buf = new Uint8Array(head.length + body.length);
      buf.set(head, 0);
      buf.set(body, head.length);
      const digest = await crypto.subtle.digest('SHA-1', buf);
      return Array.from(new Uint8Array(digest)).map(b => b.toString(16).padStart(2, '0')).join('');
    };
    const verify = async map => {
      try {
        const pins = PINS[shell];
        for (const k of Object.keys(pins)) {
          if (typeof map[k] !== 'string') continue;
          const actual = await gitBlobSha1(map[k]);
          if (cancelled) return;
          if (actual !== pins[k]) {
            console.debug('pf-mdm: source drift on ' + FILES[shell][k], {
              expected: pins[k],
              actual
            });
            setDrift({
              file: FILES[shell][k]
            });
            setStatus('drift');
            return;
          }
        }
        if (cancelled) return;
        setDrift(null);
        setSources(p => ({
          ...p,
          [shell]: map
        }));
        setStatus('ready');
      } catch (e) {
        if (!cancelled) {
          console.debug('pf-mdm: verify failed', e);
          setStatus('error');
        }
      }
    };
    const cacheKey = 'pf-mdm-' + owner + '/' + repo + '@' + branch + ':' + basePath + ':' + shell;
    try {
      const raw = sessionStorage.getItem(cacheKey);
      if (raw) {
        const c = JSON.parse(raw);
        const complete = Object.keys(FILES[shell]).every(k => typeof c.data?.[k] === 'string');
        if (complete && Date.now() - c.ts < CACHE_TTL) {
          verify(c.data);
          return () => {
            cancelled = true;
          };
        }
      }
    } catch (e) {
      console.debug('pf-mdm: cache read failed', e);
    }
    setStatus('loading');
    Promise.all(Object.entries(FILES[shell]).map(([k, rel]) => fetch(rawUrl(rel)).then(r => {
      if (!r.ok) throw new Error(rel + ': HTTP ' + r.status);
      return r.text();
    }).then(text => [k, text]))).then(pairs => {
      if (cancelled) return;
      const map = Object.fromEntries(pairs);
      try {
        sessionStorage.setItem(cacheKey, JSON.stringify({
          ts: Date.now(),
          data: map
        }));
      } catch (e) {
        console.debug('pf-mdm: cache write failed', e);
      }
      verify(map);
    }).catch(() => {
      if (!cancelled) setStatus('error');
    });
    return () => {
      cancelled = true;
    };
  }, [shell, owner, repo, branch, basePath]);
  const b64 = s => {
    try {
      return btoa(String.fromCodePoint(...new TextEncoder().encode(s)));
    } catch (e) {
      console.debug('pf-mdm: b64 failed', e);
      return '';
    }
  };
  const derive = (inp, shell) => {
    const {ns, keyId, secret, fqdn} = inp;
    let f = fqdn.trim() || 'https://factory.endorlabs.com';
    if (shell === 'powershell') f = f.replace(/\/+$/, '');
    const kid = keyId.trim();
    const sec = secret.trim();
    const host = f.replace(/^https?:\/\//, '');
    const trusted = host.replace(/:.*$/, '');
    const nsPath = '/v1/namespaces/' + ns + '/firewall/';
    const vsToken = b64(kid + ':' + sec).replace(/=/g, '').replace(/\+/g, '-').replace(/\//g, '_');
    const d = {
      NAMESPACE: ns,
      API_KEY_ID: kid,
      API_SECRET: sec,
      FQDN: f,
      FQDN_HOST: host,
      NPM_REGISTRY_URL: f + nsPath + 'npm/',
      NPM_REGISTRY_HOST: host + nsPath + 'npm/',
      PYPI_URL: f + nsPath + 'pypi/simple/',
      TRUSTED_HOST: trusted,
      MAVEN_REGISTRY_URL: f + nsPath + 'maven/',
      VSCODE_SERVICE_URL: f + nsPath + 'vscode/_ak/' + vsToken
    };
    if (shell === 'bash') d.API_SECRET_B64 = b64(sec);
    return d;
  };
  const substitute = (text, d) => {
    let out = text;
    for (const k of Object.keys(d)) out = out.split('{{' + k + '}}').join(d[k]);
    return out;
  };
  const echo = s => s + '\n';
  const buildBash = (src, eco, d, ns) => {
    const gen = src.generate;
    const heredoc = name => gen.match(new RegExp((String.raw)`cat << '${name}'\n([\s\S]*?)\n${name}\n`))[1] + '\n';
    const ARGBLOCK = heredoc('ARGBLOCK');
    const FOOTERBLOCK = heredoc('FOOTERBLOCK');
    const USERBLOCK = heredoc('USERBLOCK');
    const credRaw = gen.match(/substitute << 'CREDBLOCK'\n([\s\S]*?)\nCREDBLOCK\n/)[1] + '\n';
    const credentials = () => substitute(credRaw, d);
    const SEP_EQ = gen.match(/echo "(# ═+)"/)[1];
    const SEP_DASH = gen.match(/echo "(# ─+)"/)[1];
    const HDR_COMMON = gen.match(/echo "(# ── Common functions[^"]*)"/)[1];
    const HDR_BLOCKS = gen.match(/echo "(# ── Block content[^"]*)"/)[1];
    const descJs = gen.match(/endor-js\.sh" \\\n\s*"([^"]*)"/)[1];
    const descPy = gen.match(/endor-python\.sh" \\\n\s*"([^"]*)"/)[1];
    const descAll = gen.match(/endor-all\.sh" \\\n\s*"([^"]*)"/)[1];
    const descRm = gen.match(/"(Removes Endor Package Firewall[^"]*)"/)[1];
    const descGo = gen.match(/endor-go\.sh" \\\n\s*"([^"]*)"/)[1];
    const descMaven = gen.match(/endor-maven\.sh" \\\n\s*"([^"]*)"/)[1];
    const descVscode = gen.match(/endor-vscode\.sh" \\\n\s*"([^"]*)"/)[1];
    const doneAll = gen.match(/echo "echo \\"(\[endor\][^"]*)\\""/)[1].replace('${ENDOR_NAMESPACE}', ns);
    const inlineCommon = () => src.common.split('\n').filter(l => !l.startsWith('# ')).filter(l => !(/^[ \t]*$/).test(l)).join('\n') + '\n';
    const blockKey = {
      ENVSH_BLOCK: 'bENVSH',
      NPMRC_BLOCK: 'bNPMRC',
      YARNRC_CLASSIC_BLOCK: 'bYARNC',
      YARNRC_BLOCK: 'bYARN',
      PIP_BLOCK: 'bPIP',
      UV_BLOCK: 'bUV',
      GO_BLOCK: 'bGO',
      MAVEN_BLOCK: 'bMAVEN'
    };
    const emitBlock = v => {
      const delim = 'ENDOR_' + v;
      return echo(v + '=$(cat <<\'' + delim + '\'') + substitute(src[blockKey[v]], d) + echo('') + echo(delim) + echo(')');
    };
    const emitAllBlocks = () => echo(HDR_BLOCKS) + emitBlock('ENVSH_BLOCK') + emitBlock('NPMRC_BLOCK') + emitBlock('YARNRC_CLASSIC_BLOCK') + emitBlock('YARNRC_BLOCK') + emitBlock('PIP_BLOCK') + emitBlock('UV_BLOCK') + emitBlock('GO_BLOCK') + emitBlock('MAVEN_BLOCK') + echo(SEP_DASH) + echo('');
    const headerTop = (outName, desc) => echo('#!/usr/bin/env bash') + echo('# MDM-deployable: ' + desc) + echo('# Generated for namespace=' + ns + ' fqdn=' + d.FQDN + '.') + echo('# Do not edit — regenerate with generate.sh.') + echo('# Usage: ' + outName + ' [--dry-run]') + echo('') + echo('set -euo pipefail') + echo('');
    const header = (outName, desc) => headerTop(outName, desc) + echo(HDR_COMMON) + inlineCommon() + echo(SEP_DASH) + echo('') + ARGBLOCK + echo('') + USERBLOCK + echo('');
    const systemHeader = (outName, desc) => headerTop(outName, desc) + ARGBLOCK + echo('');
    const tpl = k => substitute(src[k], d);
    const envSetup = () => echo(SEP_EQ) + echo('# Env setup') + echo(SEP_EQ);
    if (eco === 'remove') return header('endor-remove.sh', descRm) + tpl('remove');
    if (eco === 'vscode') return systemHeader('endor-vscode.sh', descVscode) + tpl('vscode') + echo('') + FOOTERBLOCK;
    if (eco === 'all') {
      return header('endor-all.sh', descAll) + credentials() + echo('') + emitAllBlocks() + envSetup() + tpl('envsh') + echo('') + echo(SEP_EQ) + echo('# JavaScript') + echo(SEP_EQ) + tpl('js') + echo('') + echo(SEP_EQ) + echo('# Python') + echo(SEP_EQ) + tpl('python') + echo('') + echo(SEP_EQ) + echo('# Go') + echo(SEP_EQ) + tpl('go') + echo('') + echo(SEP_EQ) + echo('# Maven') + echo(SEP_EQ) + tpl('maven') + echo('') + echo(SEP_EQ) + echo('# VS Code extensions') + echo(SEP_EQ) + tpl('vscode') + echo('') + echo('echo ""') + echo('echo "' + doneAll + '"') + echo('') + FOOTERBLOCK;
    }
    const desc = ({
      js: descJs,
      python: descPy,
      go: descGo,
      maven: descMaven
    })[eco];
    return header('endor-' + eco + '.sh', desc) + credentials() + echo('') + emitAllBlocks() + envSetup() + tpl('envsh') + echo('') + tpl(eco) + echo('') + FOOTERBLOCK;
  };
  const buildPs = (src, eco, d, ns) => {
    const gen = src.generate;
    const lit = (re, fallback) => {
      const m = gen.match(re);
      return m ? m[1] : fallback;
    };
    const SEP_ENV = lit(/('# == Env vars setup =+')/, "'# == Env vars setup ='").slice(1, -1);
    const SEP_JS = lit(/('# == JavaScript =+')/, "'# == JavaScript ='").slice(1, -1);
    const SEP_PY = lit(/('# == Python =+')/, "'# == Python ='").slice(1, -1);
    const SEP_GO = lit(/('# == Go =+')/, "'# == Go ='").slice(1, -1);
    const SEP_MAVEN = lit(/('# == Maven =+')/, "'# == Maven ='").slice(1, -1);
    const SEP_VSCODE = lit(/('# == VS Code extensions =+')/, "'# == VS Code extensions ='").slice(1, -1);
    const descJs = lit(/'(Configures JavaScript[^']*)'/, 'Configures JavaScript package managers (npm, pnpm, yarn, bun) for Endor Package Firewall.');
    const descPy = lit(/'(Configures Python[^']*)'/, 'Configures Python package managers (pip, uv, poetry) for Endor Package Firewall.');
    const descGo = lit(/'(Configures Go modules[^']*)'/, 'Configures Go modules (GOPROXY) for Endor Package Firewall.');
    const descMaven = lit(/'(Configures Maven[^']*)'/, 'Configures Maven for Endor Package Firewall.');
    const descVscode = lit(/'(Configures Microsoft VS Code Stable extensions[^']*)'/, 'Configures Microsoft VS Code Stable extensions for Endor Package Firewall and installs update remediation.');
    const descAll = lit(/'(Configures all package managers[^']*)'/, 'Configures all package managers for Endor Package Firewall.');
    const descRm = lit(/'(Removes Endor Package Firewall[^']*)'/, 'Removes Endor Package Firewall configuration from all managed files and registry env vars.');
    const doneAll = lit(/Write-Host '(\[endor\] \[done\][^']*)'/, "[endor] [done] All package managers configured for $ENDOR_NAMESPACE.").replace('$ENDOR_NAMESPACE', ns);
    const footer = (gen.match(/\$ScriptFooter = @'\n([\s\S]*?)\n'@/) || [, ''])[1];
    const header = (name, desc) => {
      let r = substitute(src.header, d);
      r = r.split('{{DESCRIPTION}}').join(desc).split('{{SCRIPTNAME}}').join(name).split('{{COMMON_CONTENT}}').join(src.common);
      return r;
    };
    const sysHeaderRaw = (gen.match(/@"\n([\s\S]*?)\n"@/) || [, ''])[1];
    const systemHeader = (name, desc) => sysHeaderRaw.split('$Description').join(desc).split('$ENDOR_NAMESPACE').join(ns).split('$FQDN').join(d.FQDN).split('$ScriptName').join(name).split('`$').join('$');
    const blockKey = {
      NPMRC_BLOCK: 'bNPMRC',
      YARNRC_CLASSIC_BLOCK: 'bYARNC',
      YARNRC_BLOCK: 'bYARN',
      PIP_BLOCK: 'bPIP',
      UV_BLOCK: 'bUV',
      GO_BLOCK: 'bGO',
      MAVEN_BLOCK: 'bMAVEN'
    };
    const blockAssign = v => '$' + v + " = @'\n" + substitute(src[blockKey[v]], d).replace(/\s+$/, '') + "\n'@\n";
    const allBlocks = () => ['# -- Block content (from shared/blocks/) --', blockAssign('NPMRC_BLOCK'), blockAssign('YARNRC_CLASSIC_BLOCK'), blockAssign('YARNRC_BLOCK'), blockAssign('PIP_BLOCK'), blockAssign('UV_BLOCK'), blockAssign('GO_BLOCK'), blockAssign('MAVEN_BLOCK'), '# --', ''].join('\n');
    const tpl = k => substitute(src[k], d);
    const join = parts => parts.join('\n') + '\n';
    if (eco === 'remove') return join([header('endor-remove.ps1', descRm), tpl('remove')]);
    if (eco === 'vscode') return join([systemHeader('endor-vscode.ps1', descVscode), tpl('vscode'), footer]);
    if (eco === 'all') {
      return join([header('endor-all.ps1', descAll), allBlocks(), SEP_ENV, tpl('envvars'), '', SEP_JS, tpl('js'), '', SEP_PY, tpl('python'), '', SEP_GO, tpl('go'), '', SEP_MAVEN, tpl('maven'), '', SEP_VSCODE, tpl('vscode'), '', "Write-Host ''", "Write-Host '" + doneAll + "'", footer]);
    }
    const desc = ({
      js: descJs,
      python: descPy,
      go: descGo,
      maven: descMaven
    })[eco];
    return join([header('endor-' + eco + '.ps1', desc), allBlocks(), SEP_ENV, tpl('envvars'), '', tpl(eco), footer]);
  };
  const needsCreds = ecosystem !== 'remove';
  const cleanNs = ns.replaceAll(/[^a-zA-Z0-9._-]/g, '');
  const validNs = cleanNs.length > 0 && cleanNs === ns;
  const validInputs = validNs && (!needsCreds || keyId.trim() && secret.trim());
  const result = useMemo(() => {
    const src = sources[shell];
    if (!src || !validInputs) return null;
    try {
      const d = derive({
        ns,
        keyId,
        secret,
        fqdn: ''
      }, shell);
      const effective = {
        ...src,
        ...blockEdits
      };
      return shell === 'bash' ? buildBash(effective, ecosystem, d, ns) : buildPs(effective, ecosystem, d, ns);
    } catch (e) {
      console.debug('pf-mdm: assemble failed', e);
      return null;
    }
  }, [sources, shell, ecosystem, ns, keyId, secret, validInputs, blockEdits]);
  const ext = SHELLS[shell].ext;
  const fileName = 'endor-' + ecosystem + '.' + ext;
  const flashCopyLabel = label => {
    clearTimeout(copyTimer.current);
    setCopyLabel(label);
    copyTimer.current = setTimeout(() => setCopyLabel(COPY_LABEL), COPY_FEEDBACK_MS);
  };
  const handleCopy = () => {
    if (!result) return;
    const failed = e => {
      console.debug('pf-mdm: copy failed', e);
      flashCopyLabel('Copy failed');
    };
    if (typeof navigator.clipboard?.writeText !== 'function') {
      flashCopyLabel('Copy unavailable');
      return;
    }
    try {
      navigator.clipboard.writeText(result).then(() => flashCopyLabel('Copied')).catch(failed);
    } catch (e) {
      failed(e);
    }
  };
  const handleDownload = () => {
    if (!result) return;
    const blob = new Blob([result], {
      type: 'text/plain'
    });
    const url = URL.createObjectURL(blob);
    const a = document.createElement('a');
    a.href = url;
    a.download = fileName;
    document.body.appendChild(a);
    a.click();
    a.remove();
    URL.revokeObjectURL(url);
  };
  const switchShell = s => {
    setShell(s);
    setMdmTool(DEFAULT_TOOL[s]);
  };
  const bg = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const text = isDark ? '#e6edf3' : '#1f2937';
  const muted = isDark ? 'rgba(230,237,243,0.7)' : 'rgba(31,41,55,0.7)';
  const GRAD = 'linear-gradient(to bottom, rgba(50,225,140,0.92), rgba(30,190,110,0.92))';
  const activeFg = isDark ? '#000' : '#fff';
  const secondaryBg = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.4)';
  const DANGER_RED = '#d32f2f';
  const S = {
    card: {
      background: bg,
      border: '2px solid rgba(38,208,124,' + (isDark ? '0.4' : '0.5') + ')',
      borderRadius: '16px',
      padding: '1.25rem',
      color: text,
      boxShadow: '0 2px 8px rgba(38,208,124,0.08)'
    },
    group: {
      background: bgLight,
      borderRadius: '12px',
      padding: '0.75rem',
      border: '1px solid rgba(38,208,124,0.3)'
    },
    label: {
      display: 'block',
      fontWeight: 600,
      marginBottom: '0.25rem',
      color: text,
      fontSize: '0.8rem'
    },
    help: {
      fontSize: '0.7rem',
      color: muted,
      lineHeight: 1.3,
      marginTop: '0.25rem'
    },
    input: {
      padding: '0.5rem 0.75rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      boxSizing: 'border-box',
      outline: 'none'
    },
    select: {
      padding: '0.5rem 0.75rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      fontSize: '0.85rem',
      background: bg,
      color: text,
      width: '100%',
      cursor: 'pointer',
      outline: 'none'
    },
    toggleBtn: (active, left) => ({
      flex: 1,
      padding: '0.5rem 0.75rem',
      border: 'none',
      borderLeft: left ? '1px solid rgba(38,208,124,0.45)' : 'none',
      cursor: 'pointer',
      fontSize: '0.85rem',
      fontWeight: 600,
      background: active ? GRAD : bgLight,
      color: active ? activeFg : text
    }),
    btn: primary => ({
      padding: '0.45rem 0.9rem',
      background: primary ? GRAD : secondaryBg,
      color: primary ? activeFg : text,
      border: '1.5px solid rgba(38,208,124,0.5)',
      borderRadius: '10px',
      cursor: 'pointer',
      fontSize: '0.8rem',
      fontWeight: 500
    }),
    pre: {
      background: bg,
      color: text,
      padding: '0.75rem',
      borderRadius: '6px',
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.78rem',
      lineHeight: 1.45,
      overflowX: 'auto',
      margin: 0,
      whiteSpace: 'pre',
      border: '1px solid rgba(38,208,124,0.15)',
      maxHeight: expanded ? 'none' : OUTPUT_COLLAPSED_MAX_HEIGHT,
      overflowY: expanded ? 'auto' : 'hidden'
    },
    notice: {
      marginTop: '0.5rem',
      padding: '0.5rem 0.75rem',
      background: isDark ? '#3b2f0b' : '#fff8e6',
      border: '1px solid ' + (isDark ? '#7a5c00' : '#f5d98a'),
      borderRadius: '8px',
      color: isDark ? '#f5d98a' : '#7a5c00',
      fontSize: '0.78rem',
      lineHeight: 1.45
    },
    err: {
      padding: '0.75rem',
      background: isDark ? '#3b1111' : '#fef2f2',
      border: '1px solid ' + (isDark ? '#7f1d1d' : '#fecaca'),
      borderRadius: '8px',
      color: isDark ? '#fecaca' : '#7f1d1d',
      fontSize: '0.82rem',
      lineHeight: 1.5
    },
    code: {
      background: bgLight,
      padding: '0.1rem 0.35rem',
      borderRadius: '4px',
      fontSize: '0.8em',
      fontFamily: "'Monaco','Menlo',monospace"
    },
    textarea: {
      width: '100%',
      boxSizing: 'border-box',
      minHeight: TEXTAREA_MIN_HEIGHT,
      padding: '0.5rem 0.6rem',
      border: '1px solid rgba(38,208,124,0.45)',
      borderRadius: '8px',
      background: bg,
      color: text,
      fontFamily: "'Monaco','Menlo','Ubuntu Mono',monospace",
      fontSize: '0.72rem',
      lineHeight: 1.4,
      resize: 'vertical',
      outline: 'none'
    }
  };
  const cloneDir = basePath + '/' + shell;
  const manualCmd = shell === 'bash' ? 'git clone https://github.com/' + owner + '/' + repo + '\ncd ' + repo + '/' + cloneDir + '\nENDOR_NAMESPACE=' + (cleanNs || '<namespace>') + ' ENDOR_API_KEY_ID=<key-id> ENDOR_API_SECRET=<secret> ./generate.sh' : 'git clone https://github.com/' + owner + '/' + repo + '\ncd ' + repo + '/' + cloneDir + '\n$env:ENDOR_NAMESPACE=\'' + (cleanNs || '<namespace>') + '\'; $env:ENDOR_API_KEY_ID=\'<key-id>\'; $env:ENDOR_API_SECRET=\'<secret>\'\n./generate.ps1';
  const toolList = Object.entries(MDM[shell]);
  const tool = MDM[shell][mdmTool] || toolList[0][1];
  let missingHint = 'Enter your namespace to generate the script.';
  if (ns && !validNs) {
    missingHint = 'Namespace can contain only letters, digits, dots, hyphens, and underscores.';
  } else if (validNs) {
    missingHint = 'Enter your API key ID and secret to generate the script.';
  } else if (needsCreds) {
    missingHint = 'Enter your namespace, API key ID, and secret to generate the script.';
  }
  const srcMap = sources[shell];
  const editableBlocks = needsCreds && srcMap ? Object.keys(BLOCK_META).filter(k => BLOCK_META[k].shells.includes(shell) && BLOCK_META[k].ecos.includes(ecosystem) && srcMap[k] !== undefined) : [];
  const editedCount = editableBlocks.filter(k => blockEdits[k] !== undefined && blockEdits[k] !== srcMap[k]).length;
  return <div className="not-prose" style={{
    margin: '1.5rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div style={S.card}>
        <div style={{
    marginBottom: '0.75rem'
  }}>
          <h3 style={{
    margin: '0 0 0.25rem 0',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: text
  }}>Package Firewall MDM Script Generator</h3>
          <p style={{
    margin: 0,
    fontSize: '0.85rem',
    color: muted,
    lineHeight: 1.4
  }}>Generate a self-contained script to push via MDM. Everything runs in your browser. Your credentials are never sent anywhere.</p>
        </div>

        {status === 'drift' && <div style={S.err}>
            <strong>This generator is temporarily unavailable.</strong> The Package Firewall scripts in <code style={S.code}>{owner}/{repo}</code> changed since this page was last validated{drift?.file ? ' (' + drift.file + ')' : ''}. Generation is disabled here to avoid producing an incorrect script. Generate it from the source repo instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
            <div style={{
    marginTop: '0.5rem'
  }}>Source and instructions: <a href={'https://github.com/' + owner + '/' + repo + '/tree/' + branch + '/' + basePath} target="_blank" rel="noreferrer" style={{
    color: 'inherit',
    fontWeight: 600
  }}>github.com/{owner}/{repo}</a></div>
          </div>}

        {status !== 'drift' && <div style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(auto-fill, minmax(240px, 1fr))',
    gap: '0.75rem',
    marginBottom: '0.75rem'
  }}>
          <div style={S.group}>
            <span style={S.label} id="pf-platform">Platform</span>
            <fieldset aria-labelledby="pf-platform" style={{
    margin: 0,
    padding: 0,
    display: 'flex',
    borderRadius: '8px',
    overflow: 'hidden',
    border: '1px solid rgba(38,208,124,0.45)'
  }}>
              <button type="button" aria-pressed={shell === 'bash'} onClick={() => switchShell('bash')} style={S.toggleBtn(shell === 'bash', false)}>macOS / Linux</button>
              <button type="button" aria-pressed={shell === 'powershell'} onClick={() => switchShell('powershell')} style={S.toggleBtn(shell === 'powershell', true)}>Windows</button>
            </fieldset>
            <div style={S.help}>{SHELLS[shell].label}. Scripts run as <strong>{SHELLS[shell].runAs}</strong>.</div>
          </div>

          {!lockedEcosystem && <div style={S.group}>
            <span style={S.label} id="pf-eco">Ecosystem</span>
            <select aria-labelledby="pf-eco" value={ecosystem} onChange={e => setEcosystem(e.target.value)} style={S.select}>
              {ECOSYSTEMS.map(o => <option key={o.value} value={o.value}>{o.label}</option>)}
            </select>
            <div style={S.help}>Which package manager clients to configure (or remove configuration).</div>
          </div>}

          <div style={S.group}>
            <span style={S.label} id="pf-tool">MDM tool</span>
            <select aria-labelledby="pf-tool" value={mdmTool} onChange={e => setMdmTool(e.target.value)} style={S.select}>
              {toolList.map(([k, v]) => <option key={k} value={k}>{v.label}</option>)}
            </select>
            <div style={S.help}>Determines the upload steps shown below.</div>
          </div>

          <div style={S.group}>
            <span style={S.label}>Namespace <span style={{
    color: DANGER_RED
  }}>*</span></span>
            <input type="text" maxLength="100" aria-label="Namespace" aria-required="true" value={ns} onChange={e => setNs(e.target.value)} placeholder="your-namespace" style={S.input} />
            <div style={S.help}>Your Endor Labs namespace. Letters, digits, dots, hyphens, underscores.</div>
          </div>

          {needsCreds && <div style={S.group}>
              <span style={S.label}>API key<span style={{
    color: DANGER_RED
  }}>*</span></span>
              <input type="text" autoComplete="off" aria-label="API key ID" aria-required="true" value={keyId} onChange={e => setKeyId(e.target.value)} placeholder="key id" style={S.input} />
              <div style={S.help}>Endor Labs API key ID.</div>
            </div>}

          {needsCreds && <div style={S.group}>
              <span style={S.label}>API secret <span style={{
    color: DANGER_RED
  }}>*</span></span>
              <div style={{
    display: 'flex',
    gap: '0.35rem'
  }}>
                <input type={showSecret ? 'text' : 'password'} autoComplete="new-password" aria-label="API secret" aria-required="true" value={secret} onChange={e => setSecret(e.target.value)} placeholder="secret" style={S.input} />
                <button type="button" onClick={() => setShowSecret(v => !v)} style={{
    ...S.btn(false),
    flexShrink: 0
  }}>{showSecret ? 'Hide' : 'Show'}</button>
              </div>
              <div style={S.help}>Processed only in your browser and not sent to Endor Labs. The secret is baked into the script in plaintext.</div>
            </div>}
        </div>}

        {status === 'ready' && editableBlocks.length > 0 && <div style={{
    marginBottom: '0.75rem'
  }}>
            <button type="button" onClick={() => setShowCustomize(v => !v)} style={S.btn(false)}>{showCustomize ? '▾' : '▸'} Customize config blocks{editedCount ? ' (' + editedCount + ' edited)' : ''}</button>
            {showCustomize && <div style={{
    ...S.group,
    marginTop: '0.6rem'
  }}>
                <div style={S.help}>Edit what each Endor-managed block writes to its config file. Do not modify unless you know what you are doing. <code style={S.code}>{'${ENDOR_...}'}</code> refs resolve at runtime on the device.</div>
                {editableBlocks.map(k => <div key={k} style={{
    marginTop: '0.6rem'
  }}>
                    <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    marginBottom: '0.25rem'
  }}>
                      <span style={{
    ...S.label,
    marginBottom: 0
  }}>{BLOCK_META[k].label}</span>
                      {blockEdits[k] !== undefined && blockEdits[k] !== srcMap[k] && <button type="button" onClick={() => setBlockEdits(p => {
    const n = {
      ...p
    };
    delete n[k];
    return n;
  })} style={{
    ...S.btn(false),
    marginLeft: 'auto',
    padding: '0.2rem 0.5rem',
    fontSize: '0.72rem'
  }}>Reset</button>}
                    </div>
                    <textarea aria-label={'Customize ' + BLOCK_META[k].label} spellCheck={false} value={blockEdits[k] ?? srcMap[k]} onChange={e => setBlockEdits(p => ({
    ...p,
    [k]: e.target.value
  }))} style={S.textarea} />
                  </div>)}
              </div>}
          </div>}

        {status === 'loading' && <div style={{
    ...S.group,
    color: muted,
    fontStyle: 'italic'
  }}>Loading source from GitHub…</div>}

        {status === 'error' && <div style={S.err}>
            <strong>Couldn't read the generator from GitHub.</strong> Generate the script locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
          </div>}

        {status === 'ready' && !validInputs && <div style={{
    ...S.group,
    color: muted,
    fontSize: '0.82rem'
  }}>
            {missingHint}
          </div>}

        {status === 'ready' && validInputs && result && <div>
            <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.5rem',
    marginBottom: '0.4rem'
  }}>
              <span style={{
    fontWeight: 600,
    fontSize: '0.85rem',
    color: text
  }}>{fileName}</span>
              <button type="button" onClick={handleCopy} style={{
    ...S.btn(true),
    marginLeft: 'auto'
  }}>{copyLabel}</button>
              <button type="button" onClick={handleDownload} style={S.btn(false)}>Download</button>
            </div>
            <pre style={S.pre}>{result}</pre>
            <button type="button" onClick={() => setExpanded(v => !v)} style={{
    ...S.btn(false),
    marginTop: '0.4rem',
    width: '100%'
  }}>{expanded ? 'Show less' : 'Show full script'}</button>
            {needsCreds && <div style={S.notice}>⚠ This script contains your API key and secret in plaintext (required to write credentials at deploy time). Restrict access to the MDM policy and the downloaded file and do not commit it.</div>}
          </div>}

        {status === 'ready' && validInputs && !result && <div style={S.err}>
            <strong>Couldn't assemble the script from the current source.</strong> The generator in <code style={S.code}>{owner}/{repo}</code> may have changed in a way this page does not yet handle. Generate it locally instead:
            <pre style={{
    ...S.pre,
    marginTop: '0.5rem'
  }}>{manualCmd}</pre>
          </div>}

        {status === 'ready' && <div style={{
    ...S.group,
    marginTop: '0.75rem'
  }}>
            <span style={S.label}>Upload to {tool.label}</span>
            <ol style={{
    margin: '0.25rem 0 0 1.1rem',
    padding: 0,
    color: text,
    fontSize: '0.82rem',
    lineHeight: 1.5
  }}>
              {tool.steps.map(st => <li key={st} style={{
    marginBottom: '0.15rem'
  }}>{st}</li>)}
            </ol>
          </div>}
      </div>
    </div>;
};

<LicenseBadge sku="EL-OSS-FWAL" />

Package Firewall blocks malicious VS Code extensions before they install. It checks every extension your developers install from the Microsoft Marketplace against your policy, and any extension that does not meet it is never offered for installation.

You deploy the VS Code script to your developer machines through an MDM tool such as Microsoft Intune, Jamf, or Kandji.

View blocked and warned events for VS Code extensions in the Package Firewall logs, filtered by the VS Code ecosystem. See [View Package Firewall logs](/package-firewall/logs) to learn more.

### How it works

After you [deploy the VS Code script](#set-up-the-deployment-script) to your developer machines, Package Firewall handles extension installation requests as follows:

<Steps>
  <Step title="Point VS Code at Package Firewall">
    The VS Code script sets the `extensionsGallery.serviceUrl` in VS Code's `product.json` to your Package Firewall URL, so VS Code requests extensions from Package Firewall instead of the Microsoft Marketplace.
  </Step>

  <Step title="Filter extension requests">
    Package Firewall checks each requested extension against your policy and returns only the versions that meet it. A version that does not meet the policy is never offered for installation.
  </Step>
</Steps>

<Warning>
  VS Code rewrites `product.json` on every update, so a manually applied gallery URL is reverted. Deploy the script through an MDM tool, which re-applies the setting automatically after each VS Code update.
</Warning>

### Before you begin

Create an API key dedicated to the Package Firewall so that {authTarget_0} can authenticate to it. You can create it through one of the following methods:

* Using the Endor Labs user interface, with the **Package Firewall User** role. See [API keys](/platform-administration/api-keys#create-an-api-key-through-the-endor-labs-user-interface) to learn more.
* Using endorctl, with the `SYSTEM_ROLE_PACKAGE_FIREWALL` role. Make sure to [install and configure endorctl](/developers-api/cli/install-and-configure) before you create the key.

To create the key using endorctl, run the following command and replace:

* `<namespace>` with your namespace.
* `<API key name>` with the name of the API key for the Package Firewall use case.
* `<YYYY-MM-DDTHH:MM:SSZ>` with the API key expiration in ISO 8601 UTC format, for example `2026-12-31T23:59:59Z`.

```bash theme={null}
export NAMESPACE="<namespace>"
export KEY_NAME="<API key name>"

endorctl api create -r APIKey -n "$NAMESPACE" --data '{
  "meta": { "name": "'"$KEY_NAME"'" },
  "spec": {
    "permissions": { "roles": ["SYSTEM_ROLE_PACKAGE_FIREWALL"] },
    "expiration_time": "<YYYY-MM-DDTHH:MM:SSZ>"
  },
  "propagate": true
}'
```

### Set up the deployment script

Generate the VS Code script, then push it to your developer machines with your MDM tool. See [MDM deployment](/package-firewall/mdm-deployment) for the upload steps.

<PackageFirewallMdmGenerator lockedEcosystem="vscode" />

### Policy

A Package Firewall policy for VS Code extensions supports the malware and minimum package age conditions. When an extension version matches a condition, Package Firewall curates it out of the Microsoft Marketplace results so it is not offered for installation, rather than returning a block as it does for other ecosystems.

To always allow a specific extension, add an exception using the extension's identifier in `publisher.name` form, such as `ms-python.python`. This identifier appears as the unique identifier on the extension's Marketplace page, and in the extension's details in VS Code. You can allow all versions of the extension, a specific version, or a range of versions. See [Package Firewall policy](/package-firewall/policy) to learn more.

### Limitations

Keep the following limitations in mind when using Package Firewall for VS Code extensions:

* Package Firewall covers only the extensions available in the Microsoft Marketplace, the default for official Microsoft builds of VS Code. It does not cover the Open VSX registry, which is the default for VSCodium and Cursor.
* Extensions installed from a downloaded `.vsix` file do not go through the Marketplace, so Package Firewall cannot filter them. Disable file-based installation with a VS Code enterprise policy to close this gap.
* For an extension that publishes thousands of versions, Package Firewall may not check the oldest ones. If every version it checks is blocked, the extension cannot be installed.
