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

# Base image remediation

> Identify base image vulnerabilities in your containers and choose the update that fixes the most findings with the least risk.

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 renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={['yt-status-icon', def.colorClass].join(' ')} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  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>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    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);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = 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]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <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>
    </div>;
};

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 renderSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 2C6.48 2 2 6.48 2 12C2 17.52 6.48 22 12 22C17.52 22 22 17.52 22 12C22 6.48 17.52 2 12 2ZM10 17L5 12L6.41 10.59L10 14.17L17.59 6.58L19 8L10 17Z" fill="currentColor" />
    </svg>;
  const renderFailureIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path fillRule="evenodd" clipRule="evenodd" d="M11.9902 1.98633C12.5208 1.9864 13.0426 2.12743 13.501 2.39453C13.9581 2.66107 14.3372 3.04388 14.5986 3.50391L22.5967 17.5L22.6895 17.6738C22.8921 18.0851 22.9979 18.5387 22.9981 18.999C22.9981 19.5253 22.8605 20.0431 22.5977 20.499C22.3348 20.9549 21.9555 21.3332 21.5 21.5967C21.0445 21.8601 20.5272 21.9994 20.001 22H4.00001C3.47453 22.0031 2.95699 21.868 2.50001 21.6084C2.04032 21.3471 1.65712 20.9684 1.39063 20.5117C1.12431 20.055 0.983643 19.5355 0.982429 19.0068C0.981281 18.4793 1.11967 17.9611 1.38282 17.5039L9.38184 3.50391C9.64344 3.04359 10.023 2.66111 10.4805 2.39453C10.9388 2.12755 11.4598 1.98636 11.9902 1.98633ZM12 16.9004C11.3925 16.9004 10.9004 17.3925 10.9004 18C10.9004 18.6075 11.3925 19.0996 12 19.0996H12.0098C12.6173 19.0996 13.1104 18.6075 13.1104 18C13.1104 17.3925 12.6173 16.9004 12.0098 16.9004H12ZM12 5.90039C11.3925 5.9004 10.9004 6.39249 10.9004 7V13C10.9004 13.6075 11.3925 14.0996 12 14.0996C12.6075 14.0996 13.0996 13.6075 13.0996 13V7C13.0996 6.39249 12.6075 5.90039 12 5.90039Z" fill="currentColor" />
    </svg>;
  const renderPartialSuccessIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" aria-hidden="true">
      <path d="M12 1C18.0751 1 23 5.92487 23 12C23 18.0751 18.0751 23 12 23C5.92487 23 1 18.0751 1 12C1 5.92487 5.92487 1 12 1ZM12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3ZM12 5C13.8565 5 15.6374 5.73705 16.9502 7.0498C18.263 8.36256 19 10.1435 19 12C19 13.8565 18.263 15.6374 16.9502 16.9502C15.6374 18.263 13.8565 19 12 19C11.4477 19 11 18.5523 11 18V6C11 5.73478 11.1054 5.4805 11.293 5.29297C11.4805 5.10543 11.7348 5 12 5Z" fill="currentColor" />
    </svg>;
  const renderPendingIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
      <path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
      <path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
      <path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
      <path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
      <path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
      <path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
      <path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
      <path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
    </svg>;
  const renderRunningIcon = () => <svg className="yt-status-running-svg" viewBox="22 22 44 44" width="100%" height="100%" aria-hidden="true">
      <circle className="yt-status-running-circle" cx="44" cy="44" r="20.2" fill="none" stroke="currentColor" strokeWidth="3.6" />
    </svg>;
  const renderSkippedIcon = () => <svg viewBox="0 0 24 24" width="100%" height="100%" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
      <circle cx="12" cy="12" r="10" />
      <path d="M8 12h8" />
    </svg>;
  const STATUS_ICON_DEFS = [{
    re: /^-status-success-$/i,
    colorClass: 'yt-status-color-success',
    label: 'Success',
    render: renderSuccessIcon
  }, {
    re: /^-status-failure-$/i,
    colorClass: 'yt-status-color-failure',
    label: 'Failure',
    render: renderFailureIcon
  }, {
    re: /^-status-partial-success-$/i,
    colorClass: 'yt-status-color-partial',
    label: 'Partial success',
    render: renderPartialSuccessIcon
  }, {
    re: /^-status-pending-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Pending',
    render: renderPendingIcon
  }, {
    re: /^-status-running-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Running',
    render: renderRunningIcon
  }, {
    re: /^-status-skipped-$/i,
    colorClass: 'yt-status-color-neutral',
    label: 'Skipped',
    render: renderSkippedIcon
  }];
  const renderStatusBadge = def => <span className={['yt-status-icon', def.colorClass].join(' ')} role="img" aria-label={def.label} title={def.label}>
      {def.render()}
    </span>;
  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>;
    const statusBadge = STATUS_ICON_DEFS.find(def => def.re.test(text));
    if (statusBadge) return renderStatusBadge(statusBadge);
    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);
  const scrollWrapRef = useRef(null);
  const [isScrollable, setIsScrollable] = 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]);
  useEffect(() => {
    const wrap = scrollWrapRef.current;
    if (!wrap) return undefined;
    const updateScrollable = () => {
      setIsScrollable(wrap.scrollWidth > wrap.clientWidth);
    };
    updateScrollable();
    const observer = new ResizeObserver(updateScrollable);
    observer.observe(wrap);
    return () => observer.disconnect();
  }, [mounted, data]);
  if (!mounted) return null;
  if (!data || data.length === 0) return null;
  const rowKey = row => columns.map(c => row[c] || '').join('|');
  return <div ref={scrollWrapRef} style={{
    overflowX: 'auto',
    margin: '1.75rem 0'
  }} role={isScrollable ? 'region' : undefined} aria-label={isScrollable ? 'Scrollable table' : undefined} tabIndex={isScrollable ? 0 : undefined}>
      <table style={{
    display: 'table',
    width: '100%',
    minWidth: '100%',
    margin: 0
  }}>
        <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>
    </div>;
};

Most vulnerabilities in a container image typically come from its base image rather than your application code. Base image remediation helps you attribute findings to the base image layer and choose a base image update that resolves the most findings with the least risk.

Base image remediation is available through the `endorctl container scan` command. Endor Labs identifies the base image your application image is built on and attributes findings to the base and application layers. It then compares newer base image versions so you can plan a safe upgrade. See [Scan containers using endorctl](/scan/containers/scan-containers-using-endorctl) to set up a container scan.

## How base image remediation works

Base image remediation builds on base image detection during a container scan. The workflow has the following stages.

<Steps>
  <Step title="Base image detection">
    Container scanning identifies the base image your application image is built on. It looks for a base image you scanned earlier, one present in your container runtime, a base image label, or a name you provide with `--base-image-name` or `--dockerfile-path`. Detecting the base image separates vulnerabilities in the base image layers from those introduced by your application.
  </Step>

  <Step title="Update candidate discovery">
    Endor Labs discovers a refreshed build of the current tag, the next version, and the latest version as candidate updates for your base image. Auto-discovery of the next and latest tags is limited to base images on Docker Hub, including private repositories. For base images in other registries, point to those tags with `--base-image-tag-next` and `--base-image-tag-latest`. For more information, see [Perform base image remediation](#perform-base-image-remediation).
  </Step>

  <Step title="Impact analysis">
    Impact analysis scans each candidate base image and compares it against your current base image. The comparison reports how many findings each update would fix, how many it would introduce, and the change in findings broken down by severity and reachability.
  </Step>

  <Step title="Remediation guidance">
    Remediation guidance recommends the base image version that resolves the most findings at an acceptable risk of breaking changes. Update your project's base image reference to that version, then rebuild and rescan the image to see the updated findings. For more information, see [View update options](#view-update-options).
  </Step>
</Steps>

## Perform base image remediation

To compute the base image update options, scan your application image with the `--base-image-check-updates` flag. Choose the approach that matches how your base image is detected. Auto-discovery of the next and latest versions is best effort, so scan the base image first when you need more predictable results.

<AccordionGroup>
  <Accordion title="Base image available locally">
    When the base image is available in your local container runtime, Endor Labs detects it automatically. Run the scan with `--base-image-check-updates`.

    ```bash theme={null}
    endorctl container scan \
      --namespace=<your-namespace> \
      --image=<image_name:tag> \
      --project-name=<endor_project_name> \
      --base-image-check-updates
    ```
  </Accordion>

  <Accordion title="Base image not available locally">
    When the base image is not in your local container runtime, such as a CI environment that scans a tar file, scan the base image first as its own project. Then scan your application image with `--base-image-check-updates`. Scanning the exact base image first is the most reliable and recommended approach for CI.

    1. Scan the base image.

       ```bash theme={null}
       endorctl container scan \
         --namespace=<your-namespace> \
         --image=<base_image_name:tag> \
         --project-name=<base_image_project>
       ```

    2. Scan the application image.

       ```bash theme={null}
       endorctl container scan \
         --namespace=<your-namespace> \
         --image=<image_name:tag> \
         --project-name=<endor_project_name> \
         --base-image-check-updates
       ```
  </Accordion>

  <Accordion title="Base image with multiple tags">
    When the base image is present under multiple names or tags, use `--base-image-name` to specify which one, along with the `--base-image-check-updates` flag.

    ```bash theme={null}
    endorctl container scan \
      --namespace=<your-namespace> \
      --image=<image_name:tag> \
      --project-name=<endor_project_name> \
      --base-image-name=<base_image_name:tag> \
      --base-image-check-updates
    ```
  </Accordion>

  <Accordion title="Base image from a Dockerfile">
    To read the base image name from the Dockerfile that built the application image, use `--dockerfile-path` along with `--base-image-check-updates`. This flag is incompatible with `--base-image-name`.

    ```bash theme={null}
    endorctl container scan \
      --namespace=<your-namespace> \
      --image=<image_name:tag> \
      --project-name=<endor_project_name> \
      --dockerfile-path=<path/to/Dockerfile> \
      --base-image-check-updates
    ```
  </Accordion>
</AccordionGroup>

Pre-scanning the base image and pinning the candidate tags address different needs. Pre-scanning identifies the base image, while `--base-image-tag-next` and `--base-image-tag-latest` name the next and latest versions when auto-discovery can't determine them. Both flags require `--base-image-check-updates`, and each flag takes a single tag.

```bash theme={null}
endorctl container scan \
  --namespace=<your-namespace> \
  --image=<image_name:tag> \
  --project-name=<endor_project_name> \
  --base-image-check-updates \
  --base-image-tag-next=<next_tag> \
  --base-image-tag-latest=<latest_tag>
```

## View update options

You can view how the findings of your scanned application image would change with each base image update option. The results reflect the last scan and are not recomputed when you run this command. To recompute the update options, run the container scan with `--base-image-check-updates` again.

Each update option is a version of your base image that you can move to:

* **Refreshed**: The current base image tag resolved to its newest digest. Your version stays the same, but the image now includes the patches that were published for that tag since you last built. It's a low-risk update, shown as **Rebuild** in the Endor Labs user interface.
* **Next version**: The tag that comes immediately after your current version, in version or timestamp order. This option keeps the upgrade small, so your application sees fewer changes. It's a medium-risk update.
* **Latest version**: The newest tag available for the base image. It typically resolves the most findings of the three, but it makes the largest version change. It's a high-risk update.

Run the following command to view the update options.

```bash theme={null}
endorctl container remediation list-base-image-updates \
  --namespace=<your-namespace> \
  --image=<image_name:tag> \
  --project-name=<endor_project_name>
```

For each update option, the command reports:

* The finding counts for your current base image and the candidate, and the change between them. A negative change means fewer findings after the update.
* The findings each update fixes and the findings it introduces. A fixed finding is present now and gone on the candidate. An introduced finding is absent now and present on the candidate.

By default, results group by reachability when the scan collected reachability data. Otherwise, they group by severity. Use `--by-severity` to group by severity even when reachability data is available.

<Note>
  Reachability analysis for containers targets OS dependencies, not language packages. Most base image findings are OS packages, so reachability data helps you prioritize them. See [Container reachability](/scan/containers/container-reachability) to learn how Endor Labs measures it.
</Note>

Every update option reports one of the following statuses.

<YamlTable>
  {`


    - Summary_output: The candidate name and digest
    JSON_and_YAML_value: \`ready\`
    Description: The candidate was scanned and its impact computed. Counts and findings are available.
    - Summary_output: \`not yet scanned\`
    JSON_and_YAML_value: \`pending\`
    Description: A candidate was identified, but its impact isn't available yet. This is transient, so run the command again in a few moments.
    - Summary_output: \`not available\`
    JSON_and_YAML_value: \`not_available\`
    Description: There is no candidate for this option, such as when the tag already points to the newest digest. This status never resolves on its own.

    `}
</YamlTable>

Consider an application image built on `python:3.11.4`. Its update options might look like the following:

* **Refreshed**: Rebuild on `python:3.11.4` to include its latest patches.
* **Next version**: Upgrade to `python:3.11.5`.
* **Latest version**: Upgrade to the newest release, such as `python:3.13.1`.

The `list-base-image-updates` command supports additional flags to target the image and narrow or format the results. See [Base image update flags](#base-image-update-flags).

### Examples

The following examples show common ways to run the command, from a quick comparison of the update options to a complete review of a single candidate.

<AccordionGroup>
  <Accordion title="Triage vulnerabilities by severity">
    Show vulnerability findings only, grouped by severity. The results leave out other finding types and are organized into critical, high, medium, and low buckets, which is useful when you triage CVEs.

    ```bash theme={null}
    endorctl container remediation list-base-image-updates \
      --namespace=<your-namespace> --image=<image_name:tag> --vulns-only --by-severity
    ```
  </Accordion>

  <Accordion title="Compare update options at a glance">
    Show counts only, without individual findings. This is the fastest way to compare how many findings each update option resolves before you drill into one, because the command skips fetching the finding records.

    ```bash theme={null}
    endorctl container remediation list-base-image-updates \
      --namespace=<your-namespace> --image=<image_name:tag> --top=0
    ```
  </Accordion>

  <Accordion title="Review a single update option in full">
    Show every changed finding for a single update option. Use this to review the complete list of fixed and introduced findings for a candidate before you adopt it. The `--list-all` flag requires `--update-option`.

    ```bash theme={null}
    endorctl container remediation list-base-image-updates \
      --namespace=<your-namespace> --image=<image_name:tag> --update-option=latest --list-all
    ```
  </Accordion>

  <Accordion title="Export results for automation">
    Return the results as JSON for scripting and automation, such as gating a CI job on the number of findings an update resolves.

    ```bash theme={null}
    endorctl container remediation list-base-image-updates \
      --namespace=<your-namespace> --image=<image_name:tag> --output-type=json
    ```
  </Accordion>
</AccordionGroup>

### Base image scan flags

Use the following `endorctl container scan` flags for base image remediation.

<YamlTable>
  {`


    - Flag: \`base-image-check-updates\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_CHECK_UPDATES\`
    Type: boolean
    Description: Compute the base image update options during the scan. Required to view update options with the \`container remediation list-base-image-updates\` command.
    - Flag: \`base-image-name\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_NAME\`
    Type: string
    Description: Suggest a base image name for detection. It is used only when it matches the image's layers. It is incompatible with \`--dockerfile-path\`.
    - Flag: \`dockerfile-path\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_DOCKERFILE_PATH\`
    Type: string
    Description: Read the base image name from the Dockerfile that built the application image. It is incompatible with \`--base-image-name\`.
    - Flag: \`base-image-scan\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_SCAN\`
    Type: boolean
    Description: Scan the base image if it is identified and not previously scanned. Enabled by default.
    - Flag: \`base-image-scan-project\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_SCAN_PROJECT\`
    Type: string
    Description: Set the project name for the base image scan. If not provided, the project name is derived from the base image repository.
    - Flag: \`base-image-tag-next\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_TAG_NEXT\`
    Type: string
    Description: Pin the tag to scan as the next update candidate, instead of relying on auto-discovery. Requires \`--base-image-check-updates\` and takes a single tag.
    - Flag: \`base-image-tag-latest\`
    Environment_Variable: \`ENDOR_CONTAINER_SCAN_BASE_IMAGE_TAG_LATEST\`
    Type: string
    Description: Pin the tag to scan as the latest update candidate, instead of relying on auto-discovery. Requires \`--base-image-check-updates\` and takes a single tag.

    `}
</YamlTable>

### Base image update flags

Use the following flags with the `endorctl container remediation list-base-image-updates` command.

<YamlTable>
  {`


    - Flag: \`image\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_IMAGE\`
    Type: string
    Description: The scanned container image to analyze, using the same reference you scanned with. Required if you don't use \`--package-version-uuid\`.
    - Flag: \`package-version-uuid\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_PACKAGE_VERSION_UUID\`
    Type: string
    Description: The UUID of the scanned image's package version. Use it instead of \`--image\`, not with it.
    - Flag: \`project-name\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_PROJECT_NAME\`
    Type: string
    Description: The project the image was scanned into. Required when the image was scanned into more than one project. Incompatible with \`--package-version-uuid\`.
    - Flag: \`update-option\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_UPDATE_OPTION\`
    Type: string
    Description: Show only one update option, \`refreshed\`, \`next\`, or \`latest\`. Unset shows all three.
    - Flag: \`vulns-only\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_VULNS_ONLY\`
    Type: boolean
    Description: Show vulnerability findings only. Unset shows all finding types.
    - Flag: \`by-severity\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_BY_SEVERITY\`
    Type: boolean
    Description: Group findings by severity even when reachability data is available.
    - Flag: \`top\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_TOP\`
    Type: integer
    Description: The maximum number of changed findings shown per list, from 0 to 100. The default is 5. Set to \`0\` to show counts only.
    - Flag: \`list-all\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_LIST_ALL\`
    Type: boolean
    Description: Show every changed finding for the selected update option. Requires \`--update-option\`.
    - Flag: \`max-records\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_MAX_RECORDS\`
    Type: integer
    Description: The maximum number of records fetched per list with \`--list-all\`. The default is 1000. Set to \`0\` for unlimited.
    - Flag: \`o\`, \`output-type\`
    Environment_Variable: \`ENDOR_CONTAINER_REMEDIATION_OUTPUT_TYPE\`
    Type: string
    Description: The output format, \`summary\`, \`json\`, or \`yaml\`. The default is \`summary\`.


    `}
</YamlTable>

## View remediation results in the Endor Labs user interface

You can view the update options and remediation guidance for a scanned container image in the Endor Labs user interface.

1. Select **Containers** from the left sidebar.
2. Select a container image version to view its remediation summary.

The summary varies based on whether reachability analysis is enabled:

* If reachability analysis is enabled, the summary reports the number of reachable findings, and each update option shows how many of them remain after the update.
* If reachability analysis is not enabled, the summary ranks the findings by their total counts.

In **Base image versions**, you can view your current base image version and the update options identified for the image, each with its change in findings and a risk level:

* **Current**: Your existing base image version. It's the baseline that the update options are compared against.
* **Rebuild**: The same base image version, rebuilt to include the latest patches published for it. It's a low-risk update, shown as the refreshed option in endorctl results.
* **Next**: The version immediately after your current one. It introduces the fewest changes and rarely requires configuration updates, so it's a medium-risk update.
* **Latest**: The newest available version. It typically fixes more findings than the other options but involves the largest version change and requires a configuration review, so it's a high-risk update.

Select any version to review its findings in detail. Findings that the update resolves appear struck through, and findings that the update introduces are marked **New**. When the image was scanned with reachability analysis, the findings are grouped as **Reachable**, **Potentially reachable**, and **Non-reachable**. Otherwise, they are grouped by severity.

AURI is the Endor Labs AI assistant. Ask it about the container image and its findings to get an explanation in plain language, so you can judge an update without reading every finding yourself. Use the suggested prompts, such as *Summarize this package version* and *What stays unfixed?*, or ask your own questions. AURI answers only about the container image and its findings.

To update the base image to its next or latest version:

1. Select **Next** or **Latest** in **Base image versions**.
2. Click **Upgrade**.
3. Copy the command that matches your build setup, either a **Dockerfile** instruction or a Bazel rule for **rules\_oci** or **rules\_docker**.
4. Update the image tag in the file your project uses to define base images.
5. Rebuild the image.
6. Rescan the container image to see the refreshed results.

<img src="https://mintcdn.com/endorlabs-b4795f4f/Mrklnkvm_cNxktGc/images/scan/containers/base-image-remediation.webp?fit=max&auto=format&n=Mrklnkvm_cNxktGc&q=85&s=b6334f09c945d28a81b0542c30af5513" alt="Base image remediation" width="1632" height="1235" data-path="images/scan/containers/base-image-remediation.webp" />

You can also select **Image context** in the summary to review how the image is composed:

* Select **Layers** to view the number of findings in the base image and the number of findings in the application layer. It also shows the base image name and digest, whether an update is available, and every layer in the order it's applied with its finding count.
* Select **Metadata** to view the project, resolution time, package version UUID, exposed ports, environment variables, and labels of the image.
* Select **Dependencies** to view the dependency counts by visibility, scope, and reachability, along with the finding counts by severity.
* Select **Reachability Analysis** to view the profiling status, application type, profile type, image classification, duration, and entry point package.
