> ## 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.

# SCM Integrations

> Connect your source code management platforms for continuous monitoring and scanning.

export const SetupWizard = ({configUrl}) => {
  const [config, setConfig] = useState(null);
  const [loadError, setLoadError] = useState('');
  const [isDark, setIsDark] = useState(false);
  const [currentStep, setCurrentStep] = useState(1);
  const [selections, setSelections] = useState({});
  const [options, setOptions] = useState({});
  const [toast, setToast] = useState('');
  const [initialized, setInitialized] = useState(false);
  const loadYamlParser = () => new Promise((resolve, reject) => {
    if (globalThis.jsyaml) {
      resolve(globalThis.jsyaml);
      return;
    }
    const script = document.createElement('script');
    script.src = 'https://cdn.jsdelivr.net/npm/js-yaml@4/dist/js-yaml.min.js';
    script.onload = () => resolve(globalThis.jsyaml);
    script.onerror = () => reject(new Error('Failed to load YAML parser'));
    document.head.appendChild(script);
  });
  useEffect(() => {
    if (!configUrl) return;
    let cancelled = false;
    Promise.all([fetch(configUrl).then(r => {
      if (!r.ok) {
        throw new Error(r.status + ' ' + r.statusText);
      }
      return r.text();
    }), loadYamlParser()]).then(([yamlText, jsyaml]) => {
      if (cancelled) return;
      const data = jsyaml.load(yamlText);
      if (data?.steps) setConfig(data); else setLoadError('Invalid wizard configuration.');
    }).catch(err => {
      if (!cancelled) setLoadError('Unable to load wizard: ' + err.message);
    });
    return () => {
      cancelled = true;
    };
  }, [configUrl]);
  useEffect(() => {
    if (!config || initialized) return;
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      const raw = localStorage.getItem(sk);
      if (raw) {
        const saved = JSON.parse(raw);
        if (saved.step >= 1 && saved.step <= config.steps.length) setCurrentStep(saved.step);
        if (saved.selections) setSelections(saved.selections);
        if (saved.options) setOptions(saved.options);
      }
    } catch {}
    setInitialized(true);
  }, [config, initialized]);
  useEffect(() => {
    if (!config || !initialized) return;
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      localStorage.setItem(sk, JSON.stringify({
        step: currentStep,
        selections,
        options
      }));
    } catch {}
  }, [config, initialized, currentStep, selections, options]);
  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(() => {
    if (!toast) return;
    const t = setTimeout(() => setToast(''), 2500);
    return () => clearTimeout(t);
  }, [toast]);
  if (loadError) return <div style={{
    padding: '1rem',
    color: '#d32f2f',
    fontSize: '0.9rem'
  }}>{loadError}</div>;
  if (!config) return <div style={{
    padding: '1rem',
    opacity: 0.6,
    fontSize: '0.9rem'
  }}>Loading wizard...</div>;
  const steps = config.steps;
  const totalSteps = steps.length;
  const getSelectedItem = (dataKey, sels) => {
    const id = sels[dataKey];
    if (!id) return null;
    const items = config[dataKey];
    return items ? items.find(it => it.id === id) || null : null;
  };
  const getDocLabel = (docs, urlKey, fallback) => {
    if (!docs) return fallback || '';
    return docs[urlKey.replace(/_url$/, '_label')] || fallback || docs[urlKey] || '';
  };
  const getDocEntries = docs => {
    if (!docs) return [];
    return Object.keys(docs).filter(k => k.endsWith('_url')).map(k => ({
      key: k.replace(/_url$/, ''),
      url: docs[k],
      label: getDocLabel(docs, k, k.replace(/_url$/, ''))
    }));
  };
  const getProviderCaps = () => {
    for (const step of steps) {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item?.capabilities) return item.capabilities;
      }
    }
    return {};
  };
  const canProceed = stepNum => {
    const step = steps[stepNum - 1];
    if (!step) return false;
    if (step.type === 'choice_grid') return Boolean(selections[step.data_key]);
    if (step.type === 'plan_output') return false;
    return true;
  };
  const handleNext = () => {
    if (currentStep < totalSteps && canProceed(currentStep)) setCurrentStep(currentStep + 1);
  };
  const handleBack = () => {
    if (currentStep > 1) setCurrentStep(currentStep - 1);
  };
  const handleStepClick = n => {
    if (n <= currentStep) setCurrentStep(n);
  };
  const handleSelect = (dataKey, id) => {
    setSelections(prev => ({
      ...prev,
      [dataKey]: id
    }));
  };
  const handleOptionToggle = (id, checked) => {
    setOptions(prev => ({
      ...prev,
      [id]: checked
    }));
  };
  const handleReset = () => {
    const sk = config.wizard?.storage_key || 'setupWizard.v1';
    try {
      localStorage.removeItem(sk);
    } catch {}
    setCurrentStep(1);
    setSelections({});
    setOptions({});
    setToast('Reset complete.');
  };
  const buildChecklistLines = lines => {
    let sn = 1;
    steps.forEach(step => {
      if (step.type !== 'choice_grid') return;
      const item = getSelectedItem(step.data_key, selections);
      if (!item) return;
      if (item.pre_setup_title && item.docs) {
        lines.push(sn + ') ' + item.pre_setup_title);
        getDocEntries(item.docs).forEach(d => lines.push('   - ' + d.label + ': ' + d.url));
        if (item.warning) lines.push('   Important: ' + item.warning);
        sn++;
      } else if (item.docs) {
        if (item.docs.overview_url) {
          lines.push(sn + ') ' + getDocLabel(item.docs, 'overview_url', 'Install'), '   - ' + item.docs.overview_url);
          sn++;
        }
        if (item.docs.manage_url) {
          lines.push(sn + ') ' + getDocLabel(item.docs, 'manage_url', 'Configure'), '   - ' + item.docs.manage_url);
          sn++;
        }
      }
    });
  };
  const buildPlanText = () => {
    const lines = ['Setup plan: ' + (config.wizard.title || 'Setup Wizard'), '', 'Selections'];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item) lines.push('- ' + (step.label || step.data_key) + ': ' + (item.label || item.title || item.id));
      }
    });
    lines.push('', 'Checklist');
    buildChecklistLines(lines);
    const caps = getProviderCaps();
    const isSupported = item => {
      const capKey = item.capability_key || item.id;
      return capKey === 'pr_comments' ? Boolean(caps.pr_comments || caps.pr_checks) : Boolean(caps[capKey]);
    };
    steps.forEach(step => {
      if (step.type === 'checkbox_group') {
        const items = (config[step.data_key] || []).filter(isSupported);
        if (items.length === 0) return;
        lines.push('', step.label || 'Options');
        items.forEach(item => {
          const on = (item.id in options) ? Boolean(options[item.id]) : Boolean(item.default);
          let l = '- ' + (item.label || item.id) + ': ' + (on ? 'on' : 'off');
          if (on && item.url) l += ' (' + item.url + ')';
          lines.push(l);
        });
      }
    });
    return lines.join('\n');
  };
  const handleCopyPlan = () => {
    navigator.clipboard.writeText(buildPlanText()).then(() => setToast('Plan copied.')).catch(() => setToast('Copy failed.'));
  };
  const bgColor = isDark ? '#0d1117' : '#ffffff';
  const bgLight = isDark ? '#161b22' : '#f6f8fa';
  const textColor = isDark ? '#e6edf3' : '#1f2937';
  const bd = 'rgba(38,208,124,';
  const green = '#26D07C';
  const btnBase = {
    padding: '0.4rem 0.75rem',
    border: '1px solid',
    borderRadius: '10px',
    cursor: 'pointer',
    fontSize: '0.85rem',
    fontWeight: 600,
    display: 'inline-flex',
    alignItems: 'center',
    gap: '0.35rem',
    transition: 'all 0.15s ease'
  };
  const btnPrimary = {
    ...btnBase,
    background: green,
    borderColor: green,
    color: isDark ? '#000' : '#fff'
  };
  const btnSecondary = {
    ...btnBase,
    background: 'transparent',
    borderColor: isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.15)',
    color: textColor,
    opacity: 0.85
  };
  const codeStyle = {
    fontFamily: "Monaco, Menlo, 'Ubuntu Mono', monospace",
    fontSize: '0.85em',
    background: isDark ? 'rgba(255,255,255,0.06)' : 'rgba(0,0,0,0.04)',
    border: '1px solid ' + bd + '0.15)',
    borderRadius: '6px',
    padding: '0.1rem 0.35rem'
  };
  const h5s = {
    margin: '0.75rem 0 0.35rem',
    fontSize: '0.9rem',
    fontWeight: 700,
    opacity: 0.95
  };
  const renderChoiceGrid = step => {
    const items = config[step.data_key] || [];
    const cols = step.columns || 3;
    return <div className="sw-grid" style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(' + cols + ', 1fr)',
      gap: '0.75rem'
    }}>
        {items.map(item => {
      const sel = selections[step.data_key] === item.id;
      return <button key={item.id} type="button" onClick={() => handleSelect(step.data_key, item.id)} aria-pressed={sel} style={{
        background: sel ? bgColor : bgLight,
        border: '1px solid ' + bd + (sel ? '0.6)' : '0.15)'),
        borderRadius: '12px',
        padding: '0.75rem',
        cursor: 'pointer',
        transition: 'all 0.15s ease',
        textAlign: 'left',
        font: 'inherit',
        color: 'inherit'
      }}>
              <div style={{
        fontWeight: 700,
        marginBottom: '0.25rem',
        fontSize: '0.9rem'
      }}>{item.label || item.title || item.id}</div>
              <div style={{
        fontSize: '0.8rem',
        opacity: 0.85,
        lineHeight: 1.35
      }}>{item.description || item.summary || ''}</div>
            </button>;
    })}
      </div>;
  };
  const renderCheckboxGroup = step => {
    const caps = getProviderCaps();
    const supported = (config[step.data_key] || []).filter(item => {
      const capKey = item.capability_key || item.id;
      return capKey === 'pr_comments' ? Boolean(caps.pr_comments || caps.pr_checks) : Boolean(caps[capKey]);
    });
    return <div>
        <div className="sw-opts" style={{
      display: 'grid',
      gridTemplateColumns: 'repeat(2, 1fr)',
      gap: '0.75rem'
    }}>
          {supported.map(item => {
      const checked = (item.id in options) ? options[item.id] : Boolean(item.default);
      return <div key={item.id} style={{
        background: bgLight,
        border: '1px solid ' + bd + '0.12)',
        borderRadius: '12px',
        padding: '0.75rem'
      }}>
                <label style={{
        fontWeight: 600,
        display: 'flex',
        gap: '0.5rem',
        alignItems: 'center',
        cursor: 'pointer'
      }}>
                  <input type="checkbox" checked={checked} onChange={e => handleOptionToggle(item.id, e.target.checked)} style={{
        accentColor: green
      }} />
                  {item.label || item.id}
                </label>
                <div style={{
        fontSize: '0.78rem',
        opacity: 0.8,
        marginTop: '0.25rem',
        lineHeight: 1.35
      }}>{item.help || ''}</div>
              </div>;
    })}
        </div>
      </div>;
  };
  const renderPlanOutput = () => {
    let hasSelections = false;
    steps.forEach(step => {
      if (step.type === 'choice_grid' && getSelectedItem(step.data_key, selections)) hasSelections = true;
    });
    if (!hasSelections) return <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.12)',
      borderRadius: '12px',
      padding: '0.75rem',
      fontSize: '0.9rem'
    }}>Make selections to generate a plan.</div>;
    const selItems = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item) selItems.push({
          label: step.label || step.data_key,
          value: item.label || item.title || item.id
        });
      }
    });
    const checkItems = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (!item) return;
        if (item.pre_setup_title && item.docs) checkItems.push({
          title: item.pre_setup_title,
          docs: getDocEntries(item.docs),
          warning: item.warning
        }); else if (item.docs) {
          if (item.docs.overview_url) checkItems.push({
            title: getDocLabel(item.docs, 'overview_url', 'Installation guide'),
            url: item.docs.overview_url
          });
          if (item.docs.manage_url) checkItems.push({
            title: getDocLabel(item.docs, 'manage_url', 'Configuration guide'),
            url: item.docs.manage_url
          });
        }
      }
    });
    const optSections = [];
    steps.forEach(step => {
      if (step.type === 'checkbox_group') {
        const items = config[step.data_key] || [];
        if (items.length > 0) optSections.push({
          label: step.label || step.title || 'Options',
          items
        });
      }
    });
    const extraDocs = [];
    steps.forEach(step => {
      if (step.type === 'choice_grid') {
        const item = getSelectedItem(step.data_key, selections);
        if (item?.docs && !item.pre_setup_title) Object.keys(item.docs).forEach(k => {
          if (k.endsWith('_url') && k !== 'overview_url' && k !== 'manage_url') extraDocs.push({
            url: item.docs[k],
            label: getDocLabel(item.docs, k, k.replace(/_url$/, ''))
          });
        });
      }
    });
    return <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.12)',
      borderRadius: '12px',
      padding: '0.75rem',
      fontSize: '0.9rem',
      lineHeight: 1.45
    }}>
        <h4 style={{
      margin: '0 0 0.4rem',
      fontSize: '0.95rem',
      fontWeight: 700
    }}>Setup plan: {config.wizard.title || 'Setup Wizard'}</h4>
        <h5 style={h5s}>Selections</h5>
        <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{selItems.map(s => <li key={s.label} style={{
      margin: '0.2rem 0'
    }}>{s.label}: <code style={codeStyle}>{s.value}</code></li>)}</ul>
        <h5 style={h5s}>Checklist</h5>
        <ol style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>
          {checkItems.map(c => <li key={c.url || c.title} style={{
      margin: '0.2rem 0'
    }}>
              {c.url ? <a href={c.url} style={{
      textDecoration: 'underline'
    }}>{c.title}</a> : c.title}
              {c.docs && <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{c.docs.map(d => <li key={d.key}><a href={d.url} style={{
      textDecoration: 'underline'
    }}>{d.label}</a></li>)}</ul>}
              {c.warning && <div style={{
      marginTop: '0.35rem',
      opacity: 0.9
    }}>Important: {c.warning}</div>}
            </li>)}
        </ol>
        {optSections.map(sec => <div key={sec.label}>
            <h5 style={h5s}>{sec.label}</h5>
            <ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>
              {sec.items.filter(item => {
      const capKey = item.capability_key || item.id;
      const planCaps = getProviderCaps();
      return capKey === 'pr_comments' ? Boolean(planCaps.pr_comments || planCaps.pr_checks) : Boolean(planCaps[capKey]);
    }).map(item => {
      const on = (item.id in options) ? Boolean(options[item.id]) : Boolean(item.default);
      return <li key={item.id} style={{
        margin: '0.2rem 0'
      }}>{on && item.url ? <><a href={item.url} style={{
        textDecoration: 'underline'
      }}>{item.label || item.id}</a>: </> : (item.label || item.id) + ': '}<code style={codeStyle}>{on ? 'on' : 'off'}</code></li>;
    })}
            </ul>
          </div>)}
        {extraDocs.length > 0 && <div><h5 style={h5s}>Additional resources</h5><ul style={{
      margin: '0.25rem 0 0.25rem 1.25rem'
    }}>{extraDocs.map(d => <li key={d.url} style={{
      margin: '0.2rem 0'
    }}><a href={d.url} style={{
      textDecoration: 'underline'
    }}>{d.label}</a></li>)}</ul></div>}
      </div>;
  };
  return <div className="not-prose sw" style={{
    margin: '1rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif'
  }}>
      <div style={{
    background: bgColor,
    border: '1px solid ' + bd + '0.2)',
    borderRadius: '16px',
    padding: '1.25rem',
    color: textColor
  }}>
        <div style={{
    display: 'flex',
    gap: '0.75rem',
    alignItems: 'center',
    marginBottom: '0.75rem'
  }}>
          <div style={{
    width: '38px',
    height: '38px',
    borderRadius: '10px',
    display: 'flex',
    alignItems: 'center',
    justifyContent: 'center',
    background: bgLight,
    border: '1px solid ' + bd + '0.2)',
    color: green,
    flexShrink: 0,
    fontSize: '1.1rem'
  }}>🗺</div>
          <div>
            <h3 style={{
    margin: '0 0 0.15rem',
    fontWeight: 600,
    fontSize: '1.1rem',
    color: textColor
  }}>{config.wizard.title}</h3>
            {config.wizard.subtitle && <p style={{
    margin: 0,
    fontSize: '0.85rem',
    opacity: 0.8
  }}>{config.wizard.subtitle}</p>}
          </div>
        </div>
        <div className="sw-stepper" style={{
    display: 'grid',
    gridTemplateColumns: 'repeat(' + totalSteps + ', 1fr)',
    gap: '0.5rem',
    margin: '0.75rem 0 1rem'
  }}>
          {steps.map((step, i) => {
    const n = i + 1;
    const active = n === currentStep;
    return <button key={step.id} type="button" onClick={() => handleStepClick(n)} disabled={n > currentStep} style={{
      border: '1px solid ' + bd + (active ? '0.4)' : '0.2)'),
      borderRadius: '10px',
      padding: '0.5rem 0.6rem',
      background: active ? bgColor : bgLight,
      color: textColor,
      opacity: active ? 1 : 0.8,
      cursor: n <= currentStep ? 'pointer' : 'default',
      fontWeight: 600,
      fontSize: '0.8rem',
      display: 'inline-flex',
      alignItems: 'center',
      gap: '0.5rem',
      justifyContent: 'center'
    }}>
                <span style={{
      width: '22px',
      height: '22px',
      borderRadius: '999px',
      display: 'inline-flex',
      alignItems: 'center',
      justifyContent: 'center',
      background: 'rgba(38,208,124,0.15)',
      color: textColor,
      fontSize: '0.75rem'
    }}>{n}</span>
                {step.label}
              </button>;
  })}
        </div>
        {steps.map((step, i) => {
    const n = i + 1;
    if (n !== currentStep) return null;
    return <div key={step.id}>
              <div style={{
      fontWeight: 600,
      margin: '0 0 0.75rem',
      fontSize: '0.95rem'
    }}>{step.title}</div>
              {step.type === 'choice_grid' && renderChoiceGrid(step)}
              {step.type === 'checkbox_group' && renderCheckboxGroup(step)}
              {step.type === 'plan_output' && <div>
                  <div style={{
      display: 'flex',
      gap: '0.5rem',
      justifyContent: 'flex-end',
      flexWrap: 'wrap',
      marginBottom: '0.5rem'
    }}>
                    <button type="button" onClick={handleReset} style={btnSecondary}>↺ Reset</button>
                    <button type="button" onClick={handleCopyPlan} style={btnSecondary}>📋 Copy plan</button>
                  </div>
                  {toast && <div style={{
      background: bgLight,
      border: '1px solid ' + bd + '0.2)',
      borderRadius: '10px',
      padding: '0.5rem 0.75rem',
      margin: '0.5rem 0',
      fontSize: '0.85rem'
    }}>{toast}</div>}
                  {renderPlanOutput()}
                </div>}
            </div>;
  })}
        <div style={{
    display: 'flex',
    justifyContent: 'space-between',
    gap: '0.75rem',
    marginTop: '1rem',
    borderTop: '1px solid ' + bd + '0.1)',
    paddingTop: '1rem'
  }}>
          <button type="button" onClick={handleBack} disabled={currentStep <= 1} style={{
    ...btnSecondary,
    ...currentStep <= 1 ? {
      cursor: 'not-allowed',
      opacity: 0.5
    } : {}
  }}>Back</button>
          <button type="button" onClick={handleNext} disabled={currentStep >= totalSteps || !canProceed(currentStep)} style={{
    ...btnPrimary,
    ...currentStep >= totalSteps || !canProceed(currentStep) ? {
      cursor: 'not-allowed',
      opacity: 0.5
    } : {}
  }}>Next</button>
        </div>
      </div>
    </div>;
};

export const Diagram = ({children}) => {
  const [svg, setSvg] = useState('');
  const [error, setError] = useState(null);
  const [mounted, setMounted] = useState(false);
  const [id] = useState(() => `diagram-${Math.random().toString(36).slice(2)}`);
  const DEFAULTS = {
    theme: 'base',
    fontSize: '14px',
    fontFamily: 'inherit',
    primaryColor: '#26D07C',
    primaryTextColor: '#000000',
    primaryBorderColor: '#059669',
    secondaryColor: '#e5e7eb',
    secondaryTextColor: '#000000',
    secondaryBorderColor: '#9ca3af',
    tertiaryColor: '#e5e7eb',
    tertiaryTextColor: '#000000',
    tertiaryBorderColor: '#9ca3af',
    lineColor: '#6b7280',
    background: '#ffffff',
    edgeLabelBackground: '#f9fafb',
    clusterBkg: '#f0fdf4',
    clusterBorder: '#059669',
    nodeTextColor: '#000000',
    endorColor: '#26D07C',
    endorBorder: '#059669',
    managedColor: '#A7F3D0',
    managedBorder: '#059669',
    externalColor: '#e5e7eb',
    externalBorder: '#9ca3af'
  };
  const VAR_LINE_RE = /^(\w+):\s*(.+)$/;
  const TOKEN_RE = /\{\{(\w+)\}\}/g;
  const parseVarsBlock = raw => {
    const vars = {};
    const lines = raw.trim().split('\n');
    const diagramLines = [];
    let inVars = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '%%vars') {
        inVars = true;
        continue;
      }
      if (inVars && trimmed === '%%') {
        inVars = false;
        continue;
      }
      if (inVars) {
        const m = VAR_LINE_RE.exec(trimmed);
        if (m) vars[m[1]] = m[2];
      } else {
        diagramLines.push(line);
      }
    }
    return {
      vars,
      diagramLines
    };
  };
  const buildInitConfig = merged => ({
    theme: merged.theme,
    themeVariables: {
      fontSize: merged.fontSize,
      fontFamily: merged.fontFamily,
      primaryColor: merged.primaryColor,
      primaryTextColor: merged.primaryTextColor,
      primaryBorderColor: merged.primaryBorderColor,
      secondaryColor: merged.secondaryColor,
      secondaryTextColor: merged.secondaryTextColor,
      secondaryBorderColor: merged.secondaryBorderColor,
      tertiaryColor: merged.tertiaryColor,
      tertiaryTextColor: merged.tertiaryTextColor,
      tertiaryBorderColor: merged.tertiaryBorderColor,
      lineColor: merged.lineColor,
      background: merged.background,
      edgeLabelBackground: merged.edgeLabelBackground,
      clusterBkg: merged.clusterBkg,
      clusterBorder: merged.clusterBorder,
      nodeTextColor: merged.nodeTextColor
    }
  });
  const renderWithMermaid = (mermaid, fullDiagram) => {
    mermaid.initialize({
      startOnLoad: false,
      zoom: {
        enabled: false
      }
    });
    mermaid.render(id, fullDiagram).then(({svg: rendered}) => {
      setSvg(rendered);
      setError(null);
    }).catch(err => setError(err.message));
  };
  useEffect(() => {
    setMounted(true);
  }, []);
  useEffect(() => {
    if (!mounted || !children) return;
    const raw = typeof children === 'string' ? children : String(children);
    const {vars, diagramLines} = parseVarsBlock(raw);
    const merged = {
      ...DEFAULTS,
      ...vars
    };
    const diagram = diagramLines.join('\n').trim().replaceAll(TOKEN_RE, (_, key) => merged[key] || '');
    const fullDiagram = `%%{init: ${JSON.stringify(buildInitConfig(merged))}}%%\n${diagram}`;
    const existing = document.getElementById(id);
    if (existing) existing.remove();
    if (globalThis.mermaid) {
      renderWithMermaid(globalThis.mermaid, fullDiagram);
    } else {
      const script = document.createElement('script');
      script.src = 'https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js';
      script.onload = () => renderWithMermaid(globalThis.mermaid, fullDiagram);
      script.onerror = () => setError('Failed to load Mermaid');
      document.head.appendChild(script);
    }
  }, [mounted, children, id]);
  if (!mounted) return null;
  if (error) {
    return <pre style={{
      color: '#dc2626',
      background: '#fef2f2',
      padding: '12px',
      borderRadius: '6px',
      fontSize: '13px',
      overflowX: 'auto'
    }}>
        Diagram error: {error}
      </pre>;
  }
  if (!svg) {
    return <div style={{
      height: '200px',
      background: '#f3f4f6',
      borderRadius: '8px',
      display: 'flex',
      alignItems: 'center',
      justifyContent: 'center',
      color: '#9ca3af',
      fontSize: '14px'
    }}>
        Loading diagram...
      </div>;
  }
  return <div dangerouslySetInnerHTML={{
    __html: svg
  }} style={{
    overflowX: 'auto',
    padding: '8px 0'
  }} />;
};

export const YamlTable = ({children, data: propData, content}) => {
  const KV_RE = /^([A-Za-z][A-Za-z0-9_()/#\s-]+?):\s*(.+)$/;
  const INLINE_MD_RE = /(\[([^\]]+)\]\(([^)]+)\))|(`([^`]+)`)|(\*\*([^*]+)\*\*)|(\*([^*]+)\*)/g;
  const YES_RE = /^-yes-$/i;
  const NO_RE = /^-no-$/i;
  const LIMITED_RE = /^-(limited|partial)-$/i;
  const NA_RE = /^-(na|none)-$/i;
  const NA2_RE = /^-na2-$/i;
  const SIMPLE_TAG_RE = /(<br\s*\/?>)|(<p\s*\/?>)|(-note-)|(-warning-)/gi;
  const tryParseKV = trimmed => {
    const m = KV_RE.exec(trimmed);
    return m ? {
      key: m[1],
      value: m[2].trim()
    } : null;
  };
  const registerKey = (key, seenKeys, orderedKeys) => {
    if (!seenKeys.has(key)) {
      orderedKeys.push(key);
      seenKeys.add(key);
    }
  };
  const flushEntry = (currentEntry, entries) => {
    if (Object.keys(currentEntry).length > 0) entries.push(currentEntry);
  };
  const parseDashPrefixed = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed.startsWith('- ')) {
        if (inEntry) entries.push(currentEntry);
        currentEntry = {};
        inEntry = true;
        const kv = tryParseKV(trimmed.substring(2).trim());
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      } else if (inEntry && trimmed !== '') {
        const kv = tryParseKV(trimmed);
        if (kv) {
          registerKey(kv.key, seenKeys, orderedKeys);
          currentEntry[kv.key] = kv.value;
        }
      }
    }
    flushEntry(currentEntry, entries);
  };
  const parseBlankSeparated = (lines, entries, orderedKeys, seenKeys) => {
    let currentEntry = {};
    let inEntry = false;
    for (const line of lines) {
      const trimmed = line.trim();
      if (trimmed === '') {
        if (inEntry) {
          flushEntry(currentEntry, entries);
          currentEntry = {};
          inEntry = false;
        }
        continue;
      }
      const kv = tryParseKV(trimmed);
      if (!kv) continue;
      const isNewEntry = !line.startsWith(' ') && !line.startsWith('\t');
      if (isNewEntry && inEntry && Object.keys(currentEntry).length > 0) {
        entries.push(currentEntry);
        currentEntry = {};
      }
      registerKey(kv.key, seenKeys, orderedKeys);
      currentEntry[kv.key] = kv.value;
      inEntry = true;
    }
    flushEntry(currentEntry, entries);
  };
  const normalizeEntries = (entries, orderedKeys) => entries.map(entry => {
    const filled = {};
    for (const key of orderedKeys) filled[key] = entry[key] || '';
    return filled;
  });
  const parseYamlTableContent = contentStr => {
    if (!contentStr) return [];
    const entries = [];
    const orderedKeys = [];
    const seenKeys = new Set();
    const lines = contentStr.split('\n');
    if (lines.some(line => line.trim().startsWith('- '))) {
      parseDashPrefixed(lines, entries, orderedKeys, seenKeys);
    } else {
      parseBlankSeparated(lines, entries, orderedKeys, seenKeys);
    }
    return normalizeEntries(entries, orderedKeys);
  };
  const processText = text => {
    if (!text) return text;
    const parts = [];
    let keyIndex = 0;
    let lastIndex = 0;
    let match;
    while ((match = INLINE_MD_RE.exec(text)) !== null) {
      if (match.index > lastIndex) parts.push(text.slice(lastIndex, match.index));
      if (match[1]) {
        parts.push(<a key={keyIndex++} href={match[3]}>{match[2]}</a>);
      } else if (match[4]) {
        parts.push(<code key={keyIndex++}>{match[5]}</code>);
      } else if (match[6]) {
        parts.push(<strong key={keyIndex++}>{match[7]}</strong>);
      } else if (match[8]) {
        parts.push(<em key={keyIndex++}>{match[9]}</em>);
      }
      lastIndex = match.index + match[0].length;
    }
    if (lastIndex < text.length) parts.push(text.slice(lastIndex));
    if (parts.length === 0) return text;
    const keyRef = {
      current: keyIndex
    };
    return expandHtmlTags(parts, keyRef);
  };
  const processBadges = text => {
    if (!text || typeof text !== 'string') return text;
    if (YES_RE.test(text)) return <span className="yt-badge-yes" role="img" aria-label="Supported" title="Supported">✓</span>;
    if (NO_RE.test(text)) return <span className="yt-badge-no" role="img" aria-label="Not supported" title="Not supported">✗</span>;
    if (LIMITED_RE.test(text)) return <span className="yt-badge-limited" role="img" aria-label="Partially supported" title="Partially supported">◐</span>;
    if (NA_RE.test(text) || NA2_RE.test(text)) return <span className="yt-sr-only" title="Not applicable">Not applicable</span>;
    return processText(text);
  };
  const cellClassName = text => {
    if (!text || typeof text !== 'string') return undefined;
    if (NA_RE.test(text)) return 'yt-cell-na';
    if (NA2_RE.test(text)) return 'yt-cell-na2';
    return undefined;
  };
  const expandSimpleTags = (str, keyRef) => {
    const result = [];
    let last = 0;
    SIMPLE_TAG_RE.lastIndex = 0;
    let m;
    while ((m = SIMPLE_TAG_RE.exec(str)) !== null) {
      if (m.index > last) result.push(str.slice(last, m.index));
      if (m[1]) {
        result.push(<br key={keyRef.current++} />);
      } else if (m[2]) {
        result.push(<br key={keyRef.current++} />, <br key={keyRef.current++} />);
      } else if (m[3]) {
        result.push(<span key={keyRef.current++} className="yt-badge-note" style={{
          fontWeight: 600
        }}>Note: </span>);
      } else if (m[4]) {
        result.push(<span key={keyRef.current++} className="yt-badge-warning" style={{
          fontWeight: 600
        }}>Warning: </span>);
      }
      last = m.index + m[0].length;
    }
    if (last < str.length) result.push(str.slice(last));
    return result;
  };
  const expandHtmlTags = (chunks, keyRef) => {
    const out = [];
    for (const chunk of chunks) {
      if (typeof chunk === 'string') {
        out.push(...expandSimpleTags(chunk, keyRef));
      } else {
        out.push(chunk);
      }
    }
    return out;
  };
  const extractText = node => {
    if (node === null || node === undefined) return '';
    if (typeof node === 'string') return node;
    if (typeof node === 'number') return String(node);
    if (typeof node === 'boolean') return '';
    if (Array.isArray(node)) return node.map(extractText).join('');
    if (node && typeof node === 'object' && node.type) {
      const props = node.props || ({});
      if (typeof props.children === 'string') return props.children;
      if (props.children) return extractText(props.children);
      return '';
    }
    return String(node || '');
  };
  const [mounted, setMounted] = useState(false);
  useEffect(() => {
    setMounted(true);
  }, []);
  const data = useMemo(() => {
    if (propData) return propData;
    if (content && typeof content === 'string') return parseYamlTableContent(content);
    if (!children) return [];
    if (typeof children === 'string') return parseYamlTableContent(children);
    const childrenArray = Array.isArray(children) ? children : [children];
    return parseYamlTableContent(childrenArray.map(extractText).join('').trim());
  }, [children, propData, content]);
  const columns = useMemo(() => {
    if (!data || data.length === 0) return [];
    const firstRow = data[0];
    if (!firstRow || typeof firstRow !== 'object') return [];
    return Object.keys(firstRow);
  }, [data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <table>
      <thead>
        <tr>
          {columns.map(col => <th key={col}>{col.replaceAll('_', ' ')}</th>)}
        </tr>
      </thead>
      <tbody>
        {data.map(row => <tr key={rowKey(row)}>
            {columns.map(col => <td key={col} className={cellClassName(row[col])}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

Perform monitoring scans to gain fast and broad visibility over open source risks across the application portfolio without requiring integrations into application pipelines. These scans are conducted periodically.

<Diagram>
  {`
    graph TB
      subgraph Customer["Customer Environment"]
          REPOS[("Source Code Repositories")]
      end

      subgraph Endor[Endor Labs Infrastructure]
          APP["Endor Labs App"]
          CLOUD["Scan Environment<br/><small>Customer data destroyed after scans</small>"]
          PLATFORM["Endor Labs Platform<br/><small>Generate findings and alerts</small>"]
      end

      APP -->|1. Continuous monitoring| REPOS
      APP -->|2. Trigger scan every 24h or on-demand| CLOUD
      CLOUD <-->|3. Clone and scan repositories| REPOS
      CLOUD -->|4. Send results| PLATFORM

      style REPOS fill:#C4B5FD
    `}
</Diagram>

<Note>
  Endor Labs runs all cloud scans in a Linux environment. Projects that require platform-specific toolchains, such as Swift or Xcode for iOS and macOS, may not build or scan correctly. To scan such projects, use a [CI pipeline](/setup-deployment/ci-cd) with a runner that provides the required platform and toolchain.
</Note>

Endor Labs monitoring scans are available for the following source code management (SCM) platforms:

* **GitHub**: You can use the Endor Labs GitHub App to scan your GitHub organizations. It provides broad visibility over your GitHub organizations. Once installed, the GitHub App will automatically clone and scan all the repositories every 24 hours, providing continuous monitoring for open source vulnerabilities. It performs RSPM scans for posture management of your repository weekly on Sundays. These repositories are temporarily cloned and retained only during the scan. See [Scan using the GitHub App](/setup-deployment/scm-integrations/github-app-pro/github-app) for more information. Endor Labs supports GitHub cloud. GitHub App supports pull request scans and pull request comments. See [Scan pull requests using the GitHub App](/setup-deployment/scm-integrations/github-app-pro/github-app#configure-pr-scans-during-github-app-installation) for more information.
* **Azure DevOps**: You can use the Endor Labs Azure DevOps App to scan your Azure projects organizations. It provides broad visibility over your Azure organizations. Once installed, the Azure DevOps App will automatically clone and scan all Azure repos every 24 hours, providing continuous monitoring for open source vulnerabilities. These repositories are temporarily cloned and retained only during the scan. See [Deploy Endor Labs Azure DevOps App](/setup-deployment/scm-integrations/azure-app) for more information. Endor Labs supports Azure DevOps cloud instances.
* **GitLab**: You can use the Endor Labs GitLab App to scan your GitLab organization. It provides broad visibility over your GitLab group and subgroups. Once installed, the GitLab App will automatically clone and scan all projects every 24 hours, providing continuous monitoring for open source vulnerabilities. These repositories are temporarily cloned and retained only during the scan. See [Deploy Endor Labs GitLab App](/setup-deployment/scm-integrations/gitlab-app) for more information. Endor Labs support both GitLab cloud and self-managed instances. GitLab App supports merge request scans. See [Scan merge requests using the GitLab App](/setup-deployment/scm-integrations/gitlab-app/gitlab-mr-scan) for more information.
* **GitHub Enterprise Server**: You can use the Endor Labs GitHub App Enterprise to scan your self-hosted GitHub Enterprise Server (GHES) organizations and repositories. Once installed, the app will automatically clone and scan all repositories every 24 hours, providing continuous monitoring for open source vulnerabilities. It performs RSPM scans for posture management of your repository weekly on Sundays. These repositories are temporarily cloned and retained only during the scan. The app supports pull request scans and pull request comments as well. See [Deploy Endor Labs GitHub App Enterprise](/setup-deployment/scm-integrations/github-app-pro/github-enterprise-app) for more information.
* **Bitbucket Data Center**: You can use the Endor Labs Bitbucket App to scan your Bitbucket Data Center. It provides broad visibility over your Bitbucket projects. Once installed, the Bitbucket App will automatically clone and scan all projects every 24 hours, continuously monitoring open source vulnerabilities. These repositories are temporarily cloned and retained only during the scan. See [Deploy Endor Labs Bitbucket App for Data Center](/setup-deployment/scm-integrations/bitbucket-datacenter-app) for more information.
* **Bitbucket Cloud**: You can use the Endor Labs Bitbucket App to scan your Bitbucket Cloud. It provides broad visibility over your Bitbucket Cloud projects. Once installed, the Bitbucket App will automatically clone and scan all projects every 24 hours, providing continuous monitoring for open source vulnerabilities. These repositories are temporarily cloned and retained only during the scan. See [Deploy Endor Labs Bitbucket App for Bitbucket Cloud](/setup-deployment/scm-integrations/bitbucket-cloud) for more information.
* **Local monitoring scan**: Perform periodic scans in your local environment. You must provide the necessary computing resources to run the scans. These scans can support any type of Git repository. See [Set up Jenkins pipeline for supervisory scans](/setup-deployment/scm-integrations/jenkins-supervisory-scan).

## Monitoring scan setup wizard

You can use the following wizard to get a tailored plan for setting up monitoring scans for your SCM platform.

<SetupWizard configUrl="/snippets/setup-wizard-scm.json" />

## Support Matrix for monitoring scans

Endor Labs features available depends upon the type of scan and the SCM.

The following table lists the scan capabilities available for different types of SCM.

<YamlTable>
  {`


    - Feature: [Reachability Analysis](/scan/sca/reachability-analysis)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -yes-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [Secrets Scan](/scan/secrets)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -yes-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [SAST](/scan/sast)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -yes-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [RSPM](/scan/rspm)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -no-
    GitLab_Cloud: -no-
    GitLab_Self-Managed: -no-
    Bitbucket_Data_Center: -no-
    Bitbucket_Cloud: -no-
    - Feature: [PR Comments](/scan/pr-scans/pr-comments)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -no-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [PR Checks](/setup-deployment/scm-integrations/github-app-pro/scan-with-githubapp#scan-prs)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -no-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [Container Scan](/scan/containers)
    GitHub_Cloud: -no-
    GitHub_Enterprise_Server: -no-
    Azure_DevOps_Cloud: -no-
    GitLab_Cloud: -no-
    GitLab_Self-Managed: -no-
    Bitbucket_Data_Center: -no-
    Bitbucket_Cloud: -no-

    `}
</YamlTable>

### Remediation

The following table lists the types of remediation available for different types of SCM.

<YamlTable>
  {`


    - Feature: [Jira remediation](/integrations/jira)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -yes-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [Endor Patches](/risk-remediation/endor-patches)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -yes-
    GitLab_Cloud: -yes-
    GitLab_Self-Managed: -yes-
    Bitbucket_Data_Center: -yes-
    Bitbucket_Cloud: -yes-
    - Feature: [PR remediation](/risk-remediation/automated-pull-requests)
    GitHub_Cloud: -yes-
    GitHub_Enterprise_Server: -yes-
    Azure_DevOps_Cloud: -no-
    GitLab_Cloud: -no-
    GitLab_Self-Managed: -no-
    Bitbucket_Data_Center: -no-
    Bitbucket_Cloud: -no-

    `}
</YamlTable>

## Default branch detection

When Endor Labs scans a repository for the first time, it detects the default branch of the repository. The findings that are created in the scan are associated with the default branch.

### Changing the default branch

When you change the default branch in your source control system (for example, from `main` to `dev`):

* Endor Labs automatically detects the new default branch and sets that as the default reference
* The previous default branch becomes a reference branch
* Scans continue on the new default branch and the reference branch

The findings associated with the previous default branch are no longer associated with the default context reference. You can view them in the reference context.

### Renaming the default branch

When you rename the default branch in your source control system:

* Endor Labs automatically switches to the renamed branch
* Scans continue without disruption

### Adding repository versions

When you add a new repository version (for example, a `dev` branch), both the default branch and the new version are scanned by the Endor Labs App.

### Control default branch detection

You can control the default branch detection by setting the `ENDOR_SCAN_TRACK_DEFAULT_BRANCH` environment variable in a scan profile. You need to configure the project to use the scan profile. See [Configure scan profiles](/scan/scan-profiles) for more information.

By default, the environment variable is set to `true`. When set to `true`, the default branch detection is enabled, and the first branch you scan is automatically considered as the default branch.
