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

# Working with finding filters

> Learn how to implement and use finding filters to search, prioritize, and manage security findings across your organization.

export const FindingsFilterBuilder = ({specUrl = '/api-reference/topapi.v3.json'}) => {
  const COPY_FEEDBACK_MS = 2000;
  const PERCENT_MAX = 100;
  const WASM_URL = '/wasm/filter-engine/filter_engine.wasm.json';
  const RESOURCE = 'Finding';
  const TRANSPARENT = 'transparent';
  const COPIED = 'Copied';
  const COPY = 'Copy';
  const FAILED = 'Failed';
  const COPY_FAILED_ARIA = 'Copy failed';
  const COPY_IDLE = {
    which: '',
    ok: true
  };
  const WHICH_FILTER = 'filter';
  const WHICH_CLI = 'cli';
  const MULTI_SELECT_HINT = 'Multiple selection possible';
  const CONTROL_MAX_WIDTH = '420px';
  const MONO_FONT = 'Monaco, Menlo, Ubuntu Mono, monospace';
  const EPSS_DESCRIPTION = 'Minimum EPSS score — the probability a vulnerability will be exploited within the next 30 days.';
  const PROJECT_UUID_DESCRIPTION = 'Limit results to findings from a single project, identified by its UUID.';
  const DEP_PACKAGE_NAME_DESCRIPTION = 'One entry per selected ecosystem, in the form ecosystem://package@version.';
  const FIELD_PATHS = {
    sev: 'spec.level',
    cat: 'spec.finding_categories',
    tags: 'spec.finding_tags',
    eco: 'spec.ecosystem',
    epss: 'spec.finding_metadata.vulnerability.spec.epss_score.probability_score',
    proj: 'spec.project_uuid',
    dep: 'spec.target_dependency_package_name'
  };
  const solidBorder = color => '1px solid ' + color;
  const writeEngineString = (engineExports, value) => {
    const bytes = new TextEncoder().encode(value);
    const pointer = engineExports.wasm_alloc(bytes.length);
    new Uint8Array(engineExports.memory.buffer).set(bytes, pointer);
    return [pointer, bytes.length];
  };
  const readEngineResult = engineExports => {
    const view = new Uint8Array(engineExports.memory.buffer, engineExports.result_ptr(), engineExports.result_len());
    return JSON.parse(new TextDecoder().decode(view));
  };
  const fieldInfo = (engineExports, path) => {
    const [resourcePointer, resourceLength] = writeEngineString(engineExports, RESOURCE);
    const [pathPointer, pathLength] = writeEngineString(engineExports, path);
    try {
      engineExports.field_info(resourcePointer, resourceLength, pathPointer, pathLength);
      return readEngineResult(engineExports);
    } finally {
      engineExports.wasm_dealloc(resourcePointer, resourceLength);
      engineExports.wasm_dealloc(pathPointer, pathLength);
    }
  };
  const buildFilter = (engineExports, requestJson) => {
    const [pointer, length] = writeEngineString(engineExports, requestJson);
    try {
      engineExports.build_filter(pointer, length);
      return readEngineResult(engineExports);
    } finally {
      engineExports.wasm_dealloc(pointer, length);
    }
  };
  const ENUM_KEY_PATTERN = /^[A-Z0-9_]+$/;
  const parseEnumDescriptions = description => {
    const descriptions = {};
    String(description || '').split('\n').forEach(line => {
      const trimmed = line.trimStart();
      if (!trimmed.startsWith('-')) return;
      const afterDash = trimmed.slice(1).trimStart();
      const colonIndex = afterDash.indexOf(':');
      if (colonIndex <= 0) return;
      const key = afterDash.slice(0, colonIndex);
      if (!ENUM_KEY_PATTERN.test(key)) return;
      descriptions[key] = afterDash.slice(colonIndex + 1).trim();
    });
    return descriptions;
  };
  const humanizeEnum = (value, prefix) => {
    const text = value.replace(prefix, '').toLowerCase().replaceAll('_', ' ');
    return text.replace(/\b\w/g, character => character.toUpperCase());
  };
  const base64ToUint8Array = binary => Uint8Array.from(binary, character => character.codePointAt(0));
  const instantiateFilterEngine = wrapper => {
    const bytes = base64ToUint8Array(globalThis.atob(wrapper.content));
    return globalThis.WebAssembly.instantiate(bytes);
  };
  const cacheFilterEngineExports = ({instance}) => {
    if (instance.exports._initialize) instance.exports._initialize();
    globalThis.__endorWasm = {
      ...globalThis.__endorWasm,
      filterEngine: instance.exports
    };
    return instance.exports;
  };
  const readOkJson = (response, label) => {
    if (!response.ok) throw new Error(label + ' HTTP ' + response.status);
    return response.json();
  };
  const readOkText = (response, label) => {
    if (!response.ok) throw new Error(label + ' HTTP ' + response.status);
    return response.text();
  };
  const loadCatalogFromSpec = (engineExports, specText) => {
    const [pointer, length] = writeEngineString(engineExports, specText);
    let schemaCount;
    try {
      schemaCount = engineExports.init_spec(pointer, length);
    } finally {
      engineExports.wasm_dealloc(pointer, length);
    }
    if (!schemaCount) throw new Error('OpenAPI schema initialization failed');
    const info = {};
    Object.entries(FIELD_PATHS).forEach(([key, path]) => {
      info[key] = fieldInfo(engineExports, path);
      if (!info[key].valid) throw new Error('Finding field is missing: ' + path);
    });
    ['sev', 'cat', 'tags', 'eco'].forEach(key => {
      if (!Array.isArray(info[key].enum)) {
        throw new TypeError('Finding enum is missing: ' + FIELD_PATHS[key]);
      }
    });
    return info;
  };
  const [isDark, setIsDark] = useState(false);
  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();
  }, []);
  const [selection, setSelection] = useState({
    sev: [],
    cat: [],
    tags: [],
    eco: []
  });
  const [epss, setEpss] = useState(0);
  const [projectUuid, setProjectUuid] = useState('');
  const [depPackageNames, setDepPackageNames] = useState({
    '': ''
  });
  const [copyState, setCopyState] = useState(COPY_IDLE);
  const [openTooltipId, setOpenTooltipId] = useState(null);
  const [engine, setEngine] = useState(null);
  const [catalog, setCatalog] = useState(null);
  const [loadError, setLoadError] = useState('');
  const [generation, setGeneration] = useState({
    valid: true,
    filter: '',
    error: ''
  });
  const uid = 'ffb';
  const ids = {
    sev: uid + '-sev',
    cat: uid + '-cat',
    tags: uid + '-tags',
    eco: uid + '-eco',
    epss: uid + '-epss',
    proj: uid + '-proj',
    dep: uid + '-dep'
  };
  const copyTimer = useRef(null);
  useEffect(() => () => {
    if (copyTimer.current) clearTimeout(copyTimer.current);
  }, []);
  useEffect(() => {
    const abortController = new AbortController();
    let isActive = true;
    setCatalog(null);
    setEngine(null);
    setLoadError('');
    const loadEngine = () => {
      const cached = globalThis.__endorWasm?.filterEngine;
      if (cached) return Promise.resolve(cached);
      return fetch(WASM_URL, {
        signal: abortController.signal
      }).then(response => readOkJson(response, 'WASM')).then(instantiateFilterEngine).then(cacheFilterEngineExports);
    };
    const loadSpec = () => fetch(specUrl, {
      signal: abortController.signal
    }).then(response => readOkText(response, 'OpenAPI'));
    Promise.all([loadEngine(), loadSpec()]).then(([engineExports, specText]) => {
      if (!isActive) return;
      setCatalog(loadCatalogFromSpec(engineExports, specText));
      setEngine(engineExports);
    }).catch(error => {
      if (isActive && error.name !== 'AbortError') {
        setLoadError('Unable to load the findings filter engine: ' + error.message);
      }
    });
    return () => {
      isActive = false;
      abortController.abort();
    };
  }, [specUrl]);
  const ECO_PREFIXES = {
    ECOSYSTEM_C: 'c',
    ECOSYSTEM_CARGO: 'cargo',
    ECOSYSTEM_COCOAPOD: 'cocoapod',
    ECOSYSTEM_CONTAINER: 'container',
    ECOSYSTEM_GEM: 'gem',
    ECOSYSTEM_GITHUB_ACTION: 'githubaction',
    ECOSYSTEM_GO: 'go',
    ECOSYSTEM_HUGGING_FACE: 'hfmodel',
    ECOSYSTEM_MAVEN: 'mvn',
    ECOSYSTEM_NPM: 'npm',
    ECOSYSTEM_NUGET: 'nuget',
    ECOSYSTEM_PACKAGIST: 'packagist',
    ECOSYSTEM_PYPI: 'pypi',
    ECOSYSTEM_SWIFT: 'swift'
  };
  useEffect(() => {
    setDepPackageNames(prev => {
      if (selection.eco.length === 0) {
        return {
          '': prev[''] || ''
        };
      }
      const next = {};
      selection.eco.forEach(eco => {
        const prefix = ECO_PREFIXES[eco] ? ECO_PREFIXES[eco] + '://' : '';
        next[eco] = (eco in prev) ? prev[eco] : prefix;
      });
      return next;
    });
  }, [selection.eco]);
  const LABEL_OVERRIDES = {
    FINDING_CATEGORY_SCPM: 'RSPM',
    FINDING_CATEGORY_CICD: 'CI/CD',
    FINDING_CATEGORY_GHACTIONS: 'GitHub Actions',
    FINDING_CATEGORY_AI_MODELS: 'AI models',
    FINDING_CATEGORY_SCA: 'SCA',
    FINDING_CATEGORY_SAST: 'SAST',
    FINDING_TAGS_EXPLOITED: 'Exploited (KEV)',
    FINDING_TAGS_CI_BLOCKER: 'Break the build',
    FINDING_TAGS_CI_WARNING: 'Warning',
    ECOSYSTEM_C: 'C/C++',
    ECOSYSTEM_COCOAPOD: 'CocoaPods',
    ECOSYSTEM_GEM: 'RubyGems',
    ECOSYSTEM_GITHUB_ACTION: 'GitHub Action',
    ECOSYSTEM_HUGGING_FACE: 'Hugging Face',
    ECOSYSTEM_MAVEN: 'Maven',
    ECOSYSTEM_NPM: 'npm',
    ECOSYSTEM_NUGET: 'NuGet',
    ECOSYSTEM_PYPI: 'PyPI'
  };
  const TAG_GROUP_CONFIG = [['Reachability', ['FINDING_TAGS_REACHABLE_FUNCTION', 'FINDING_TAGS_REACHABLE_DEPENDENCY', 'FINDING_TAGS_POTENTIALLY_REACHABLE_FUNCTION', 'FINDING_TAGS_UNREACHABLE_FUNCTION']], ['Fix status', ['FINDING_TAGS_FIX_AVAILABLE', 'FINDING_TAGS_FIXABLE', 'FINDING_TAGS_UNFIXABLE']], ['Exploitability', ['FINDING_TAGS_EXPLOITED', 'FINDING_TAGS_DISPUTED', 'FINDING_TAGS_WITHDRAWN']], ['Dependency', ['FINDING_TAGS_DIRECT', 'FINDING_TAGS_TRANSITIVE', 'FINDING_TAGS_SELF', 'FINDING_TAGS_PHANTOM', 'FINDING_TAGS_SEGMENT_MATCH']], ['Scope', ['FINDING_TAGS_NORMAL', 'FINDING_TAGS_TEST']], ['Secrets & policy', ['FINDING_TAGS_VALID_SECRET', 'FINDING_TAGS_INVALID_SECRET', 'FINDING_TAGS_MALWARE', 'FINDING_TAGS_CI_BLOCKER', 'FINDING_TAGS_CI_WARNING', 'FINDING_TAGS_EXCEPTION']]];
  const enumItems = (fieldKey, prefix) => {
    if (!catalog?.[fieldKey]?.enum) return [];
    const info = catalog[fieldKey];
    const descriptions = parseEnumDescriptions(info.enum_description || info.description);
    return info.enum.filter(value => !value.endsWith('_UNSPECIFIED')).map(value => [value, LABEL_OVERRIDES[value] || humanizeEnum(value, prefix), descriptions[value] || info.description || '']);
  };
  const SEVERITIES = enumItems('sev', 'FINDING_LEVEL_');
  const CATEGORIES = enumItems('cat', 'FINDING_CATEGORY_');
  const TAGS = enumItems('tags', 'FINDING_TAGS_');
  const ECOSYSTEMS = enumItems('eco', 'ECOSYSTEM_');
  const tagsByValue = Object.fromEntries(TAGS.map(item => [item[0], item]));
  const TAG_GROUPS = TAG_GROUP_CONFIG.map(([label, values]) => [label, values.map(value => tagsByValue[value]).filter(Boolean)]).filter(([, items]) => items.length > 0);
  const epssDescription = catalog?.epss?.description || EPSS_DESCRIPTION;
  const projectUuidDescription = catalog?.proj?.description || PROJECT_UUID_DESCRIPTION;
  const dependencyNameDescription = catalog?.dep?.description || DEP_PACKAGE_NAME_DESCRIPTION;
  const PRESETS = [['Critical/high, reachable & fixable', {
    sev: ['FINDING_LEVEL_CRITICAL', 'FINDING_LEVEL_HIGH'],
    tags: ['FINDING_TAGS_REACHABLE_FUNCTION', 'FINDING_TAGS_FIX_AVAILABLE']
  }, 'Critical or high severity findings that are reachable and have a fix available.'], ['Known-exploited (KEV)', {
    tags: ['FINDING_TAGS_EXPLOITED']
  }, "Findings tagged as actively exploited per CISA's KEV catalog."], ['Reachable vulns, EPSS > 10%', {
    cat: ['FINDING_CATEGORY_VULNERABILITY'],
    tags: ['FINDING_TAGS_REACHABLE_FUNCTION'],
    epss: 10
  }, 'Reachable vulnerabilities with an EPSS exploit probability above 10%.'], ['Malware', {
    cat: ['FINDING_CATEGORY_MALWARE']
  }, 'Findings categorized as malware.'], ['Valid secrets', {
    cat: ['FINDING_CATEGORY_SECRETS'],
    tags: ['FINDING_TAGS_VALID_SECRET']
  }, 'Secrets findings confirmed as valid.'], ['Direct deps, fix available', {
    tags: ['FINDING_TAGS_DIRECT', 'FINDING_TAGS_FIX_AVAILABLE']
  }, 'Findings in direct dependencies that have a fix available.'], ['KEV-exploited, no fix', {
    tags: ['FINDING_TAGS_EXPLOITED', 'FINDING_TAGS_UNFIXABLE']
  }, 'Actively exploited findings with no fix available.'], ['Supply chain, direct deps', {
    cat: ['FINDING_CATEGORY_SUPPLY_CHAIN'],
    tags: ['FINDING_TAGS_DIRECT']
  }, 'Supply chain findings in direct dependencies.'], ['License risk', {
    cat: ['FINDING_CATEGORY_LICENSE_RISK']
  }, 'Findings categorized as license risk.'], ['CI/CD break-the-build', {
    tags: ['FINDING_TAGS_CI_BLOCKER']
  }, 'Findings tagged to break the CI/CD build.'], ['Container findings', {
    cat: ['FINDING_CATEGORY_CONTAINER']
  }, 'Findings detected in container images.'], ['SAST, reachable', {
    cat: ['FINDING_CATEGORY_SAST'],
    tags: ['FINDING_TAGS_REACHABLE_FUNCTION']
  }, 'Reachable SAST findings in first-party code.']];
  const availableValues = {
    sev: new Set(SEVERITIES.map(item => item[0])),
    cat: new Set(CATEGORIES.map(item => item[0])),
    tags: new Set(TAGS.map(item => item[0])),
    eco: new Set(ECOSYSTEMS.map(item => item[0]))
  };
  const presetSupported = preset => ['sev', 'cat', 'tags', 'eco'].every(group => (preset[1][group] || []).every(value => availableValues[group].has(value)));
  const supportedPresets = PRESETS.filter(presetSupported);
  const SELECTION_MODE = {
    sev: 'multi',
    cat: 'multi',
    tags: 'multi',
    eco: 'multi'
  };
  const toggle = (group, val) => {
    setSelection(prev => {
      const current = prev[group];
      const has = current.includes(val);
      let next;
      if (SELECTION_MODE[group] === 'single') {
        next = has ? [] : [val];
      } else {
        next = has ? current.filter(v => v !== val) : current.concat([val]);
      }
      return {
        ...prev,
        [group]: next
      };
    });
  };
  const applyPreset = preset => {
    setSelection({
      sev: preset.sev || [],
      cat: preset.cat || [],
      tags: preset.tags || [],
      eco: preset.eco || []
    });
    setEpss(preset.epss || 0);
    setProjectUuid(preset.proj || '');
    setDepPackageNames(preset.depPackageNames || ({
      '': ''
    }));
  };
  const sameSet = (a, b) => a.length === b.length && a.every(v => b.includes(v));
  const isPresetActive = preset => {
    const p = preset[1];
    return sameSet(selection.sev, p.sev || []) && sameSet(selection.cat, p.cat || []) && sameSet(selection.tags, p.tags || []) && sameSet(selection.eco, p.eco || []) && epss === (p.epss || 0) && projectUuid === (p.proj || '') && JSON.stringify(depPackageNames) === JSON.stringify(p.depPackageNames || ({
      '': ''
    }));
  };
  const clearAll = () => {
    setSelection({
      sev: [],
      cat: [],
      tags: [],
      eco: []
    });
    setEpss(0);
    setProjectUuid('');
    setDepPackageNames({
      '': ''
    });
  };
  const buildFilterRequest = () => {
    const clauses = [];
    if (selection.sev.length) clauses.push({
      path: FIELD_PATHS.sev,
      operator: 'in',
      values: selection.sev
    });
    if (selection.cat.length) clauses.push({
      path: FIELD_PATHS.cat,
      operator: 'contains',
      values: selection.cat
    });
    selection.tags.forEach(tag => clauses.push({
      path: FIELD_PATHS.tags,
      operator: 'contains',
      values: [tag]
    }));
    if (selection.eco.length) clauses.push({
      path: FIELD_PATHS.eco,
      operator: 'in',
      values: selection.eco
    });
    if (epss > 0) clauses.push({
      path: FIELD_PATHS.epss,
      operator: '>',
      values: [epss / PERCENT_MAX]
    });
    if (projectUuid.trim()) clauses.push({
      path: FIELD_PATHS.proj,
      operator: '==',
      values: [projectUuid.trim()]
    });
    const allEcoPrefixes = new Set(Object.values(ECO_PREFIXES).map(p => p + '://'));
    const dependencyValues = Object.values(depPackageNames).map(value => value.trim()).filter(value => value && !allEcoPrefixes.has(value));
    if (dependencyValues.length === 1) {
      clauses.push({
        path: FIELD_PATHS.dep,
        operator: '==',
        values: dependencyValues
      });
    } else if (dependencyValues.length > 1) {
      clauses.push({
        path: FIELD_PATHS.dep,
        operator: 'in',
        values: dependencyValues
      });
    }
    return {
      resource: RESOURCE,
      clauses
    };
  };
  const filterRequestJson = JSON.stringify(buildFilterRequest());
  const filterText = generation.valid ? generation.filter : '';
  const shellQuote = value => "'" + value.replaceAll("'", "'\"'\"'") + "'";
  const cliText = filterText ? 'endorctl api list --resource Finding --filter ' + shellQuote(filterText) : '';
  const epssLabel = epss > 0 ? '> ' + epss + '%' : '0';
  useEffect(() => {
    if (!engine) {
      setGeneration({
        valid: true,
        filter: '',
        error: ''
      });
      return;
    }
    try {
      const nextGeneration = buildFilter(engine, filterRequestJson);
      setGeneration({
        valid: Boolean(nextGeneration.valid),
        filter: nextGeneration.valid ? nextGeneration.filter || '' : '',
        error: nextGeneration.valid ? '' : nextGeneration.error || 'Filter generation failed'
      });
    } catch (error) {
      setGeneration({
        valid: false,
        filter: '',
        error: error.message || 'Filter generation failed'
      });
    }
  }, [engine, filterRequestJson]);
  const handleCopy = (which, text) => {
    if (!text) return;
    const finish = ok => {
      setCopyState({
        which,
        ok
      });
      if (copyTimer.current) clearTimeout(copyTimer.current);
      copyTimer.current = setTimeout(() => setCopyState(COPY_IDLE), COPY_FEEDBACK_MS);
    };
    const fallbackCopy = () => {
      try {
        const ta = document.createElement('textarea');
        ta.value = text;
        ta.setAttribute('readonly', '');
        ta.style.cssText = 'position:fixed;left:-9999px;opacity:0';
        document.body.appendChild(ta);
        ta.select();
        const ok = document.execCommand('copy');
        ta.remove();
        finish(ok);
      } catch (e) {
        console.debug('ffb: fallback copy failed', e);
        finish(false);
      }
    };
    if (navigator.clipboard?.writeText) {
      navigator.clipboard.writeText(text).then(() => finish(true)).catch(fallbackCopy);
    } else {
      fallbackCopy();
    }
  };
  const WHITE = '#ffffff';
  const bgMain = isDark ? '#1e1e1e' : WHITE;
  const bgSection = isDark ? '#161b22' : WHITE;
  const textColor = isDark ? '#e6edf3' : '#111827';
  const mutedColor = isDark ? 'rgba(230,237,243,0.75)' : '#6b7280';
  const labelSectionColor = isDark ? '#e6edf3' : '#4b5563';
  const inputBg = isDark ? '#0d1117' : WHITE;
  const MINT_BORDER_LIGHT = '#a8e6cf';
  const MINT_TEXT_LIGHT = '#1b7a55';
  const borderOuter = isDark ? 'rgba(38, 208, 124, 0.35)' : MINT_BORDER_LIGHT;
  const borderInput = isDark ? 'rgba(38, 208, 124, 0.35)' : '#d1d5db';
  const borderSection = isDark ? 'rgba(38, 208, 124, 0.2)' : MINT_BORDER_LIGHT;
  const accentMint = isDark ? 'rgba(38, 208, 124, 0.85)' : MINT_TEXT_LIGHT;
  const accentMintBorder = isDark ? 'rgba(38, 208, 124, 0.5)' : MINT_BORDER_LIGHT;
  const copyIconColor = isDark ? 'rgba(38, 208, 124, 0.9)' : MINT_TEXT_LIGHT;
  const copyFailColor = isDark ? '#f85149' : '#dc2626';
  const chipSelBg = isDark ? 'rgba(38, 208, 124, 0.24)' : 'rgba(168, 230, 207, 0.6)';
  const chipSelText = isDark ? 'rgba(120, 230, 180, 0.95)' : MINT_TEXT_LIGHT;
  const presetIdleBg = isDark ? 'rgba(38, 208, 124, 0.08)' : 'rgba(168, 230, 207, 0.15)';
  const containerStyle = {
    background: bgMain,
    border: solidBorder(borderOuter),
    borderRadius: '16px',
    padding: '1.25rem',
    margin: '1rem 0',
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    boxShadow: 'none',
    color: textColor
  };
  const sectionStyle = {
    marginBottom: '0.75rem',
    background: bgSection,
    borderRadius: '12px',
    padding: '0.75rem',
    border: solidBorder(borderSection)
  };
  const labelStyle = {
    display: 'inline-block',
    fontWeight: 500,
    marginBottom: '0.45rem',
    color: labelSectionColor,
    fontSize: '0.8rem',
    textTransform: 'uppercase',
    letterSpacing: '0.5px'
  };
  const sectionLabelRowStyle = {
    display: 'flex',
    alignItems: 'baseline',
    flexWrap: 'wrap',
    gap: '0.5rem',
    marginBottom: '0.45rem'
  };
  const sectionLabelTextStyle = {
    ...labelStyle,
    marginBottom: 0
  };
  const hintStyle = {
    color: mutedColor,
    fontSize: '0.72rem',
    fontWeight: 400,
    textTransform: 'none',
    letterSpacing: 'normal'
  };
  const infoIconBtnStyle = {
    display: 'inline-flex',
    alignItems: 'center',
    justifyContent: 'center',
    width: '1rem',
    height: '1rem',
    padding: 0,
    border: 'none',
    borderRadius: '50%',
    background: TRANSPARENT,
    color: mutedColor,
    cursor: 'pointer'
  };
  const TOOLTIP_WIDTH = '200px';
  const infoTooltipStyle = {
    position: 'absolute',
    top: 'calc(100% + 0.4rem)',
    left: '50%',
    transform: 'translateX(-50%)',
    zIndex: 3,
    padding: '0.4rem 0.6rem',
    borderRadius: '6px',
    fontSize: '0.72rem',
    lineHeight: 1.5,
    whiteSpace: 'normal',
    width: 'max-content',
    maxWidth: TOOLTIP_WIDTH,
    textAlign: 'left',
    pointerEvents: 'none',
    background: isDark ? '#0d1117' : '#111827',
    color: '#e6edf3',
    border: solidBorder(borderSection)
  };
  const tagGroupLabelStyle = {
    color: mutedColor,
    fontSize: '0.72rem',
    margin: '0 0 0.3rem',
    letterSpacing: '0.3px'
  };
  const chipsWrapStyle = {
    display: 'flex',
    flexWrap: 'wrap',
    gap: '0.4rem'
  };
  const groupResetStyle = {
    margin: 0,
    padding: 0,
    border: 0,
    minWidth: 0
  };
  const chipStyle = (selected, dim) => ({
    fontFamily: 'inherit',
    fontSize: '0.8rem',
    lineHeight: 1.3,
    whiteSpace: 'nowrap',
    padding: '0.3rem 0.7rem',
    borderRadius: '999px',
    cursor: 'pointer',
    border: solidBorder(selected ? accentMintBorder : borderInput),
    background: selected ? chipSelBg : TRANSPARENT,
    color: selected ? chipSelText : textColor,
    fontWeight: selected ? 500 : 400,
    opacity: dim ? 0.45 : 1
  });
  const controlStyle = {
    width: '100%',
    padding: '0.5rem 0.75rem',
    border: solidBorder(borderInput),
    borderRadius: '8px',
    fontSize: '0.85rem',
    background: inputBg,
    color: textColor,
    boxSizing: 'border-box',
    outline: 'none'
  };
  const previewBoxStyle = {
    position: 'relative',
    background: inputBg,
    border: solidBorder(borderSection),
    borderRadius: '8px',
    padding: '0.75rem',
    paddingRight: '2.75rem',
    marginTop: '0.5rem'
  };
  const preStyle = {
    margin: 0,
    whiteSpace: 'pre-wrap',
    wordBreak: 'break-all',
    fontFamily: MONO_FONT,
    fontSize: '0.8rem',
    lineHeight: 1.5,
    color: isDark ? textColor : '#374151'
  };
  const btnOutline = {
    padding: '0.25rem 0.6rem',
    fontSize: '0.75rem',
    fontWeight: 500,
    borderRadius: '8px',
    cursor: 'pointer',
    border: solidBorder(accentMintBorder),
    background: TRANSPARENT,
    color: accentMint
  };
  const btnCopyStyle = {
    position: 'absolute',
    top: '0.5rem',
    right: '0.5rem',
    background: TRANSPARENT,
    border: 'none',
    color: copyIconColor,
    cursor: 'pointer',
    padding: '0.25rem',
    fontSize: '0.78rem',
    fontWeight: 500
  };
  const placeholderColor = mutedColor;
  const renderInfoTooltip = (id, description) => {
    if (!description) return null;
    const tooltipId = id + '-info';
    const isOpen = openTooltipId === id;
    return <span style={{
      position: 'relative',
      display: 'inline-flex',
      alignItems: 'center'
    }}>
        <button type="button" aria-label="Field description" aria-describedby={isOpen ? tooltipId : undefined} style={infoIconBtnStyle} onMouseEnter={() => setOpenTooltipId(id)} onMouseLeave={() => setOpenTooltipId(prev => prev === id ? null : prev)} onFocus={() => setOpenTooltipId(id)} onBlur={() => setOpenTooltipId(prev => prev === id ? null : prev)} onKeyDown={e => {
      if (e.key === 'Escape') setOpenTooltipId(prev => prev === id ? null : prev);
    }}>
          <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
            <circle cx="12" cy="12" r="10"></circle>
            <line x1="12" y1="16" x2="12" y2="12"></line>
            <line x1="12" y1="8" x2="12.01" y2="8"></line>
          </svg>
        </button>
        {isOpen && <span id={tooltipId} role="tooltip" style={infoTooltipStyle}>
            {description}
          </span>}
      </span>;
  };
  const renderSectionLabel = (id, text, hint, description) => <span style={sectionLabelRowStyle}>
      <span id={id} style={sectionLabelTextStyle}>{text}</span>
      {hint ? <span style={hintStyle}>{hint}</span> : null}
      {renderInfoTooltip(id, description)}
    </span>;
  const renderChips = (group, items) => <div style={chipsWrapStyle}>
      {items.map(pair => {
    const isSelected = selection[group].includes(pair[0]);
    const dim = SELECTION_MODE[group] === 'single' && selection[group].length > 0 && !isSelected;
    return <button key={pair[0]} type="button" className="ffb-chip" aria-pressed={isSelected} title={pair[2]} style={chipStyle(isSelected, dim)} onClick={() => toggle(group, pair[0])}>
            {pair[1]}
          </button>;
  })}
    </div>;
  const renderTagGroups = () => TAG_GROUPS.map(tagGroup => <div key={tagGroup[0]} style={{
    marginBottom: '0.55rem'
  }}>
      <div style={tagGroupLabelStyle}>{tagGroup[0]}</div>
      {renderChips('tags', tagGroup[1])}
    </div>);
  const copyButtonText = which => {
    if (copyState.which !== which) return COPY;
    return copyState.ok ? COPIED : FAILED;
  };
  const copyButtonAria = (which, idleAria) => {
    if (copyState.which !== which) return idleAria;
    return copyState.ok ? COPIED : COPY_FAILED_ARIA;
  };
  const copyButtonStyle = which => {
    if (copyState.which === which && !copyState.ok) {
      return {
        ...btnCopyStyle,
        color: copyFailColor
      };
    }
    return btnCopyStyle;
  };
  const themeClass = isDark ? 'ffb-dark' : 'ffb-light';
  if (loadError) {
    return <div className="not-prose findings-filter-builder" role="alert" style={containerStyle}>
        <strong>Findings Filter Builder unavailable.</strong>
        <div style={{
      marginTop: '0.35rem',
      color: mutedColor
    }}>{loadError}</div>
        <div style={{
      marginTop: '0.35rem',
      color: mutedColor
    }}>
          Refresh the page, or enter a filter directly in the Findings <strong>Advanced filter</strong> box.
        </div>
      </div>;
  }
  if (!catalog) {
    return <div className="not-prose findings-filter-builder" aria-live="polite" style={containerStyle}>
        Loading Finding fields from the API specification…
      </div>;
  }
  return <div className={'not-prose findings-filter-builder ' + themeClass} style={containerStyle}>
      <style>
        {`
          .findings-filter-builder .ffb-chip {
            transition: background-color 0.12s, border-color 0.12s, opacity 0.12s;
          }
          .findings-filter-builder.ffb-light .ffb-chip:hover { border-color: ${MINT_BORDER_LIGHT}; }
          .findings-filter-builder.ffb-dark .ffb-chip:hover { border-color: rgba(38, 208, 124, 0.55); }
          .findings-filter-builder .ffb-chip:focus-visible {
            outline: 2px solid rgba(38, 208, 124, 0.5);
            outline-offset: 1px;
          }
          .findings-filter-builder.ffb-light input::placeholder { color: #9ca3af; }
          .findings-filter-builder.ffb-dark input::placeholder { color: rgba(230, 237, 243, 0.45); }
          .findings-filter-builder.ffb-light .ffb-control:focus {
            border-color: ${MINT_BORDER_LIGHT};
            box-shadow: 0 0 0 2px rgba(168, 230, 207, 0.45);
          }
          .findings-filter-builder.ffb-dark .ffb-control:focus {
            border-color: rgba(38, 208, 124, 0.55);
            box-shadow: 0 0 0 2px rgba(38, 208, 124, 0.2);
          }
          .findings-filter-builder .ffb-epss { accent-color: ${accentMint}; }
        `}
      </style>

      <div style={{
    marginBottom: '1rem'
  }}>
        <div style={{
    color: textColor,
    margin: '0 0 0.25rem',
    fontWeight: 600,
    fontSize: '1.1rem'
  }}>
          Findings Filter Builder
        </div>
        <p style={{
    color: mutedColor,
    margin: 0,
    fontSize: '0.85rem',
    lineHeight: 1.4
  }}>
          Select attributes to build an advanced filter, then copy the expression into the Findings <strong style={{
    color: textColor
  }}>Advanced filter</strong> box or run it with <code>endorctl</code>.
        </p>
        <p style={{
    color: mutedColor,
    margin: '0.4rem 0 0',
    fontSize: '0.8rem',
    lineHeight: 1.4
  }}>
          To filter dismissed findings, use the <a href="/inventory-insights/findings/dismiss-findings#filter-dismissed-findings" style={{
    color: accentMint
  }}>Findings UI</a>.
        </p>
      </div>

      <div style={sectionStyle}>
        <span style={{
    ...labelStyle,
    marginBottom: '0.5rem'
  }}>Quick starts</span>
        <div style={chipsWrapStyle}>
          {supportedPresets.map(preset => {
    const isActive = isPresetActive(preset);
    return <button key={preset[0]} type="button" aria-pressed={isActive} title={preset[2]} style={{
      ...btnOutline,
      border: solidBorder(isActive ? accentMintBorder : borderInput),
      background: isActive ? chipSelBg : presetIdleBg,
      color: isActive ? chipSelText : textColor
    }} onClick={() => applyPreset(preset[1])}>
                {preset[0]}
              </button>;
  })}
        </div>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.sev, 'Severity', MULTI_SELECT_HINT)}
        <fieldset aria-labelledby={ids.sev} style={groupResetStyle}>{renderChips('sev', SEVERITIES)}</fieldset>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.cat, 'Category', MULTI_SELECT_HINT)}
        <fieldset aria-labelledby={ids.cat} style={groupResetStyle}>{renderChips('cat', CATEGORIES)}</fieldset>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.tags, 'Attributes', 'Multiple selection possible; all selected must match')}
        <fieldset aria-labelledby={ids.tags} style={{
    ...groupResetStyle,
    marginTop: '0.2rem'
  }}>{renderTagGroups()}</fieldset>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.eco, 'Ecosystem', MULTI_SELECT_HINT)}
        <fieldset aria-labelledby={ids.eco} style={groupResetStyle}>{renderChips('eco', ECOSYSTEMS)}</fieldset>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.epss, 'EPSS probability', 'Single threshold', epssDescription)}
        <div style={{
    display: 'flex',
    alignItems: 'center',
    gap: '0.75rem',
    maxWidth: CONTROL_MAX_WIDTH,
    marginTop: '0.2rem'
  }}>
          <input type="range" className="ffb-epss" min="0" max={PERCENT_MAX} step="1" value={epss} aria-labelledby={ids.epss} aria-valuetext={epssLabel} title={epssDescription} style={{
    flex: 1
  }} onChange={e => setEpss(Number.parseInt(e.target.value, 10) || 0)} />
          <span style={{
    fontSize: '0.85rem',
    fontWeight: 500,
    minWidth: '64px',
    color: textColor
  }}>
            {epssLabel}
          </span>
        </div>
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.proj, 'Project UUID', 'Single value', projectUuidDescription)}
        <input type="text" className="ffb-control" placeholder="660e2bc48c7d4e60a5fc692f" value={projectUuid} aria-labelledby={ids.proj} title={projectUuidDescription} style={{
    ...controlStyle,
    maxWidth: CONTROL_MAX_WIDTH,
    cursor: 'text',
    color: textColor
  }} onChange={e => setProjectUuid(e.target.value)} />
      </div>

      <div style={sectionStyle}>
        {renderSectionLabel(ids.dep, 'Dependency package name', null, <span>
            One entry per selected ecosystem.
            <br />
            e.g. <span style={{
    fontFamily: MONO_FONT
  }}>ecosystem://package@version</span>
          </span>)}
        <div style={{
    marginTop: '0.2rem'
  }}>
          {(selection.eco.length === 0 ? [''] : selection.eco).map(eco => {
    const ecoLabel = eco ? (ECOSYSTEMS.find(e => e[0] === eco) || [])[1] : null;
    const ecosystemPrefix = ECO_PREFIXES[eco];
    let placeholder = 'npm://lodash@4.17.21';
    if (eco) {
      placeholder = ecosystemPrefix ? ecosystemPrefix + '://package-name@version' : 'package-name@version';
    }
    return <div key={eco || 'generic'} style={{
      display: 'flex',
      alignItems: 'center',
      gap: '0.5rem',
      marginBottom: '0.4rem'
    }}>
                {ecoLabel && <span style={{
      fontSize: '0.75rem',
      color: mutedColor,
      minWidth: '72px',
      textAlign: 'right',
      flexShrink: 0
    }}>
                    {ecoLabel}
                  </span>}
                <input type="text" className="ffb-control" placeholder={placeholder} value={depPackageNames[eco] !== undefined ? depPackageNames[eco] : ''} aria-label={ecoLabel ? ecoLabel + ' dependency package name' : 'Dependency package name'} title={dependencyNameDescription} style={{
      ...controlStyle,
      maxWidth: CONTROL_MAX_WIDTH,
      cursor: 'text',
      color: textColor
    }} onChange={e => setDepPackageNames(prev => ({
      ...prev,
      [eco]: e.target.value
    }))} />
              </div>;
  })}
        </div>
      </div>

      <div style={sectionStyle}>
        <span style={labelStyle}>Advanced filter</span>
        <div style={previewBoxStyle}>
          <pre style={{
    ...preStyle,
    color: filterText ? preStyle.color : placeholderColor
  }}>
            {filterText || 'Select attributes above to build a query.'}
          </pre>
          <button type="button" disabled={!filterText || !generation.valid} style={{
    ...copyButtonStyle(WHICH_FILTER),
    opacity: !filterText || !generation.valid ? 0.45 : 1,
    cursor: !filterText || !generation.valid ? 'not-allowed' : 'pointer'
  }} onClick={() => handleCopy(WHICH_FILTER, filterText)} aria-label={copyButtonAria(WHICH_FILTER, 'Copy advanced filter to clipboard')} title={filterText && generation.valid ? 'Copy to clipboard' : undefined}>
            {copyButtonText(WHICH_FILTER)}
          </button>
        </div>
        {!generation.valid && <p role="alert" style={{
    fontSize: '0.8rem',
    color: copyFailColor,
    margin: '0.5rem 0 0'
  }}>
            The filter engine could not generate this expression: {generation.error}
          </p>}
        <p style={{
    fontSize: '0.8rem',
    color: mutedColor,
    margin: '0.5rem 0 0'
  }}>
          Paste this into the <strong style={{
    color: textColor
  }}>Advanced filter</strong> box on the Findings page.
        </p>
      </div>

      <div style={sectionStyle}>
        <span style={labelStyle}>endorctl command</span>
        <div style={previewBoxStyle}>
          <pre style={{
    ...preStyle,
    color: cliText ? preStyle.color : placeholderColor
  }}>
            {cliText || 'Select attributes above to generate a command.'}
          </pre>
          <button type="button" disabled={!cliText || !generation.valid} style={{
    ...copyButtonStyle(WHICH_CLI),
    opacity: !cliText || !generation.valid ? 0.45 : 1,
    cursor: !cliText || !generation.valid ? 'not-allowed' : 'pointer'
  }} onClick={() => handleCopy(WHICH_CLI, cliText)} aria-label={copyButtonAria(WHICH_CLI, 'Copy endorctl command to clipboard')} title={cliText && generation.valid ? 'Copy to clipboard' : undefined}>
            {copyButtonText(WHICH_CLI)}
          </button>
        </div>
      </div>

      <div>
        <button type="button" style={btnOutline} onClick={clearAll}>
          Clear all
        </button>
      </div>
    </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 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>;
};

Filters enable targeted queries on findings based on attributes such as severity, category, reachability, ecosystem, and policy status.

This guide explains how finding filters work, how to apply and combine them effectively, and provides practical examples to support triage, audit, and reporting workflows across your finding inventory.

## How filters work

Each filter consists of three parts.

* **Key**: The attribute you want to filter (for example, `Severity`, `Category`, and `Ecosystems`).
* **Operator**: The comparison logic (for example, `equals`, `in`, and `contains`).
* **Value**: The target value to evaluate (for example, `Critical` and `Vulnerability`).

Finding filters use standard comparison operators to evaluate criteria. See [Filter operators](/developers-api/rest-api/using-the-rest-api/filters#operators) for detailed information about available operators and their usage when using the API.

## Filter implementation techniques

You can use the following filter types to manage your findings effectively.

* [Basic filters](#search-for-findings-using-basic-filters): Use the filter bar in the Endor Labs user interface to quickly segment findings by common attributes.
* [Advanced filters](#search-for-findings-using-advanced-filters): Use advanced filters to create powerful queries that drill deeper into the dataset to fetch results with a specific context.
* [Saved filters](#saved-filters): Save your custom filters for reuse across projects.

### Search for findings using basic filters

Use the following basic filters to search for information in your findings.

* **Finding Level**: Limit results by finding severity level.
* **Dismissed**: Include or exclude dismissed findings. See [Filter dismissed findings](/inventory-insights/findings/dismiss-findings#filter-dismissed-findings) to learn more about filtering dismissed findings.
* **Category**: Choose from CI/CD, Malware, license risks, operational risks, RSPM, GitHub Actions, SAST, AI models, containers, secrets, security, supply chain, or vulnerability and view related findings.
* **Projects**: Narrow findings by one or more project names.
* **Custom Tags**: Narrow down the list based on custom tags.
* **Attributes**: Narrow down the list based on a range of factors such as:
  * if a patch is available to fix the findings
  * if the vulnerable function is reachable
  * if the dependency is reachable
  * if the dependency originates from a current repository or a current tenant
  * if the dependency is a test dependency
  * if the dependency's discovery type is manifest, phantom, or segment match
  * if the finding originates from itself, direct, or a transitive dependency
  * if the SAST finding is generated by the AI SAST detection agent
  * if AI SAST analysis has classified the SAST finding as a true positive or false positive
  * filter the findings by the **Exploited** tag from **CISA KEV**
  * filter the findings by the **Warn** or **Break the Build** options set in the [action policy](/platform-administration/policies/action-policies#create-an-action-policy-from-template)
* **EPSS Probability**: Choose the Exploit Prediction Scoring System (EPSS) score range.
* **Ecosystems**: Filter by language or ecosystem.
* **Location**: Narrow findings by where they occur (for example, path or location in the repository).
* **Confidence**: Narrow findings by detection confidence.
* **SAST Languages**: Narrow SAST findings by programming language.
* **Container Layers**: Narrow container findings by image layer.
* **Remediation**: Narrow vulnerability findings by fix status.
  * **Endor Patch Available**: Filters findings where an Endor-provided patch is available to fix the vulnerability.
  * **Recommended Upgrade Available**: Filters findings where a recommended version upgrade is available.
* **All Time**: Choose a time range.

### Search for findings using advanced filters

For complex queries, use the advanced filter syntax to combine multiple attributes and apply logical operators. Toggle the **Advanced** option in the filter bar to enter API-style filter expressions directly in the Endor Labs application.

<Tip>
  Search using the advanced filters applies to all the branches of a repository. You can retrieve results from any branch by specifying the relevant context ID or type. See [View findings associated with a project](/inventory-insights/findings#view-findings-associated-with-a-project) for an example of scoping findings to a specific branch.
</Tip>

The **Advanced Filters** use the `GetFinding` [API call](/api-reference/findingservice/getfinding) to fetch results. The following table lists the available attributes for finding filters.

<YamlTable>
  {`


    - Title: Severity Level
    Key: \`spec.level\`
    Description: Filters by severity level, such as Critical, High, Medium, or Low.

    - Title: Dismissed
    Key: \`spec.dismiss\`
    Description: Filters by whether the finding has been dismissed.

    - Title: Categories
    Key: \`spec.finding_categories\`
    Description: Filters by finding category, such as vulnerability, supply chain, license risk, secrets, malware, CI/CD, SAST, containers, or AI models.

    - Title: Attributes
    Key: \`spec.finding_tags\`
    Description: Filters by behavioral and contextual tags, such as fix available, reachable function, reachable dependency, direct, transitive, test, exploited, or unfixable.

    - Title: Ecosystem
    Key: \`spec.ecosystem\`
    Description: Filters by the package ecosystem where the issue was detected, such as npm, Maven, PyPI, or Go.

    - Title: Project UUID
    Key: \`spec.project_uuid\`
    Description: Filters by the project to which the finding belongs. Use to scope results to a single project.

    - Title: Finding Name
    Key: \`meta.name\`
    Description: Filters by the finding's name identifier.

    - Title: Parent UUID
    Key: \`meta.parent_uuid\`
    Description: Filters by the UUID of the parent object: a package version, repository version, or repository.

    - Title: Dependency Package Name
    Key: \`spec.target_dependency_package_name\`
    Description: Filters by the fully qualified name of the affected dependency package version, in the format \`ecosystem://package@version\`.

    - Title: Dependency Name
    Key: \`spec.target_dependency_name\`
    Description: Filters by the name of the affected dependency package, without the ecosystem prefix or version.

    - Title: Dependency Version
    Key: \`spec.target_dependency_version\`
    Description: Filters by the version of the affected dependency package.

    - Title: EPSS Score
    Key: \`spec.finding_metadata.vulnerability.spec.epss_score.probability_score\`
    Description: Filters by EPSS probability score, expressed as a decimal between 0 and 1 (for example, \`0.1\` equals 10%).

    - Title: Endor Patch Available
    Key: \`spec.fixing_patch.endor_patch_available\`
    Description: Filters by whether an Endor Labs-provided patch is available to fix the vulnerability.

    - Title: Approximation
    Key: \`spec.approximation\`
    Description: Filters by whether the affected dependency was discovered through approximate resolution rather than full manifest parsing.

    - Title: Context ID
    Key: \`context.id\`
    Description: Filters by the scan context identifier, such as a branch or tag. Use to retrieve findings from a specific branch or repository version.

    - Title: Last Processed
    Key: \`spec.last_processed\`
    Description: Filters by the timestamp of when the finding was last processed by the system.


    `}
</YamlTable>

#### Interactive filter builder

<FindingsFilterBuilder specUrl="/api-reference/topapi.v3.json" />

#### API filter use cases

The following examples demonstrate how to combine these attributes for common security and compliance workflows.

<AccordionGroup>
  <Accordion title="Prioritize critical and high severity fixable findings">
    Find critical and high-severity findings where a fix is available and the vulnerable function is reachable.

    ```bash theme={null}
    spec.level in ["FINDING_LEVEL_CRITICAL","FINDING_LEVEL_HIGH"] and spec.finding_tags contains ["FINDING_TAGS_FIX_AVAILABLE"] and spec.finding_tags contains ["FINDING_TAGS_REACHABLE_FUNCTION"]
    ```
  </Accordion>

  <Accordion title="Find high-probability exploitable vulnerabilities">
    Identify vulnerability findings with an EPSS score greater than 10% to focus on issues most likely to be exploited in the wild.

    ```bash theme={null}
    spec.finding_categories contains ["FINDING_CATEGORY_VULNERABILITY"] and spec.finding_metadata.vulnerability.spec.epss_score.probability_score > 0.1
    ```
  </Accordion>

  <Accordion title="Scope findings to a specific project">
    Retrieve all active, non-dismissed vulnerability findings for a single project.

    ```bash theme={null}
    spec.finding_categories contains ["FINDING_CATEGORY_VULNERABILITY"] and spec.project_uuid == "<project-uuid>" and spec.dismiss == false
    ```
  </Accordion>

  <Accordion title="Find vulnerabilities in a specific ecosystem">
    List all vulnerability findings from PyPI packages across the namespace.

    ```bash theme={null}
    spec.finding_categories contains ["FINDING_CATEGORY_VULNERABILITY"] and spec.ecosystem in ["ECOSYSTEM_PYPI"]
    ```
  </Accordion>

  <Accordion title="Audit dismissed findings">
    Review all dismissed findings across the namespace to verify exception policies are applied correctly.

    ```bash theme={null}
    spec.dismiss == true
    ```
  </Accordion>

  <Accordion title="Find findings from a specific branch">
    Retrieve findings scoped to a specific branch or repository version by providing the context ID.

    ```bash theme={null}
    context.id == "<context-id>"
    ```
  </Accordion>

  <Accordion title="Identify KEV-listed exploited findings with no fix">
    Find findings tied to CVEs in the CISA KEV database that have no available fix, to assess unmitigatable risk.

    ```bash theme={null}
    spec.finding_tags contains ["FINDING_TAGS_EXPLOITED"] and spec.finding_tags contains ["FINDING_TAGS_UNFIXABLE"]
    ```
  </Accordion>

  <Accordion title="Find supply chain findings for direct dependencies">
    Surface supply chain findings that affect only direct dependencies to prioritize the most actionable issues.

    ```bash theme={null}
    spec.finding_categories contains ["FINDING_CATEGORY_SUPPLY_CHAIN"] and spec.finding_tags contains ["FINDING_TAGS_DIRECT"]
    ```
  </Accordion>
</AccordionGroup>

### Saved filters

Saved filters are customizable filter settings that users can create and reuse across projects in Endor Labs. They improve efficiency by eliminating the need to manually recreate filters. You can save the advanced search filters that you created to fetch curated search results. Saved queries are visible in the drop-down list.

To create a saved filter:

1. Select **Findings** from the left sidebar.
2. Toggle **Advanced** in the top right corner.
3. Type or build your query in the filter bar.
4. Click **Saved Filters** in the top right corner.
5. Click **Save Current**.
6. Enter a name in the **Choose filter name** field.
7. Click **Save**.

<img src="https://mintcdn.com/endorlabs-b4795f4f/2_cgBAK0fOLHo0lP/images/inventory-insights/findings/saved-filter.webp?fit=max&auto=format&n=2_cgBAK0fOLHo0lP&q=85&s=5c417194cbe44cee81285ed17efa8d69" alt="Create saved filter" style={{width: '80%'}} width="1900" height="390" data-path="images/inventory-insights/findings/saved-filter.webp" />

#### Manage saved filters

To delete a saved filter:

1. Select **User menu** > **Settings** from the left sidebar.
2. Select **Saved Filters**.
3. Click the vertical three dots on the right side of the filter you want to delete and click **Delete**.

To edit a saved filter:

1. Select **User menu** > **Settings** from the left sidebar.
2. Select **Saved Filters**.
3. Click **Edit** next to the filter you want to edit.
4. You can update the name, query, and tags.
5. Click **Update** to save the updated changes.

<img src="https://mintcdn.com/endorlabs-b4795f4f/2_cgBAK0fOLHo0lP/images/inventory-insights/findings/update-saved-filter.webp?fit=max&auto=format&n=2_cgBAK0fOLHo0lP&q=85&s=bdaadb350ba87a94fd35aee124ba34ee" alt="Update saved filter" style={{width: '60%'}} width="1486" height="886" data-path="images/inventory-insights/findings/update-saved-filter.webp" />
