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

# Export findings to Wiz

> Learn how to export findings from Endor Labs to Wiz to enable code-to-cloud correlation in the Wiz Security Graph.

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>;
};

Endor Labs provides a holistic view of your code and software supply chain security so you can focus on the findings that matter most. Endor Labs pushes findings to Wiz after every scheduled scan on the default branch and maps them to Wiz’s enrichment schemas.

Export findings from Endor Labs to Wiz by establishing a secure connection with Wiz API endpoints. The integration sends SCA and SAST findings identified during repository scans to Wiz, and Wiz ingests them into the Wiz Security Graph.

<Note>
  **Branch restrictions**

  Wiz exports apply only to the default branch. Currently, you cannot export pull request scans and non-default branch scans.
</Note>

## Prerequisites

Ensure that the following prerequisites are complete:

* To use the Endor Labs integration, your Wiz tenant must have the **Wiz Code License**.

* Connect your source code manager to Wiz so that Wiz scans repositories and findings become available. Wiz currently supports the following providers:

  * GitHub
  * GitLab
  * Bitbucket
  * Azure DevOps

  This connection ensures that `REPOSITORY_BRANCH` assets exist in Wiz's inventory. Without this connection, Wiz accepts findings but **SKIPS** them during ingestion because Wiz cannot resolve the repository.

* Add the [Endor Labs integration](https://app.wiz.io/settings/automation/integrations) from the Wiz Integration Network. When you create the integration, Wiz shows the required API scopes for the service account. Save the following values because you will need them when creating the Wiz exporter in Endor Labs:
  * Client ID
  * Client Secret
  * API Endpoint URL
  * Authentication URL

* Download and install endorctl. See [Install endorctl](/introduction/getting-started).

## Create a Wiz exporter

Create a Wiz exporter with the Endor Labs API to configure the export destination and data types.

The following table lists the configuration options required to create the exporter.

<YamlTable>
  {`


    - Parameter: \`<namespace>\`
    Description: Your Endor Labs namespace.
    - Parameter: \`<exporter-name>\`
    Description: A descriptive name for the exporter.
    - Parameter: \`<api-endpoint-url>\`
    Description: Your Wiz tenant's region-specific GraphQL endpoint. For example, \`https://api.us18.app.wiz.io/graphql\`.
    - Parameter: \`<auth-endpoint-url>\`
    Description: The Wiz authentication endpoint. For example, \`https://auth.app.wiz.io/oauth/token\`.
    - Parameter: \`<client-id>\`
    Description: The Client ID from your Wiz service account.
    - Parameter: \`<client-secret>\`
    Description: The Client Secret from your Wiz service account.


    `}
</YamlTable>

Run the following command to create a Wiz exporter.

```bash expandable theme={null}
endorctl api create \
  --namespace=<namespace> \
  --resource=Exporter \
  --data '{
    "meta": {
      "name": "<exporter-name>"
    },
    "propagate": true,
    "spec": {
      "exporter_type": "EXPORTER_TYPE_WIZ",
      "wiz_config": {
        "api_endpoint_url": "<api-endpoint-url>",
        "oauth_client_credentials": {
          "auth_endpoint_url": "<auth-endpoint-url>",
          "client_id": "<client-id>",
          "client_secret": "<client-secret>"
        }
      },
      "message_type_configs": [
        {
          "message_type": "MESSAGE_TYPE_FINDING",
          "message_export_format": "MESSAGE_EXPORT_FORMAT_JSON"
        }
      ]
    }
  }'
```

For example, to create a Wiz exporter named `wiz-findings-export` in the namespace `doe.deer` that exports findings to Wiz:

```bash expandable theme={null}
endorctl api create \
  --namespace=doe.deer \
  --resource=Exporter \
  --data '{
    "meta": {
      "name": "wiz-findings-export"
    },
    "propagate": true,
    "spec": {
      "exporter_type": "EXPORTER_TYPE_WIZ",
      "wiz_config": {
        "api_endpoint_url": "https://api.us18.app.wiz.io/graphql",
        "oauth_client_credentials": {
          "auth_endpoint_url": "https://auth.app.wiz.io/oauth/token",
          "client_id": "your-wiz-client-id",
          "client_secret": "your-wiz-client-secret"
        }
      },
      "message_type_configs": [
        {
          "message_type": "MESSAGE_TYPE_FINDING",
          "message_export_format": "MESSAGE_EXPORT_FORMAT_JSON"
        }
      ]
    }
  }'
```

## Configure the scan profile to use the Wiz exporter

After creating the exporter, associate it with your scan profile. You can also set the scan profile as the default for your namespace so all projects use it automatically. See [Scan profiles](/scan/scan-profiles) for more information.

### Configure the scan profile

1. Select **User menu** > **Settings** from the left sidebar.
2. Select **Scan Profiles**.
3. Select the scan profile you want to configure and click **Edit Scan Profile**.
4. Select your exporter under **Exporters** and click **Save Scan Profile**.

### Configure the project to use the scan profile

Associate your project with a scan profile to enable automatic export of scan data.

1. Select **Projects** from the left sidebar and select the project you want to configure.
2. Select **Settings** and select the scan profile you want to use under **Scan Profile**.

## View findings in Wiz

Once Wiz successfully ingests findings, you can view them directly in the Wiz **Findings** dashboard. Wiz correlates these findings with your cloud assets and repositories in the Wiz Security Graph. You can filter by origin to easily locate findings from Endor Labs.

## Finding lifecycle in Wiz

Endor Labs exports findings to Wiz after every scheduled scan on the default branch. Wiz manages finding state based on data sources:

* **Full state snapshot**: Each upload represents the complete current state of findings for a project and branch. Wiz treats it as a full replacement.
* **Upload limit**: Wiz allows up to three uploads per branch per day. Wiz may not process additional uploads for the same branch within 24 hours.
* **Automatic resolution**: If a finding was present in a previous upload but is absent in the current upload for the same scope, Wiz automatically marks it as resolved and closes any associated Wiz Issues.
* **Staleness**: Findings not refreshed within 7 days are automatically removed by Wiz. Wiz recommends uploading at least every 24 hours to align with their scanning cycle.

## Ingestion status

When an upload completes, Wiz processes the results asynchronously.

The following table lists the ingestion outcomes.

<YamlTable>
  {`


    - Status: \`PENDING\`
    Meaning: Upload is queued; processing has not started.
    - Status: \`IN_PROGRESS\`
    Meaning: Wiz is still processing the file.
    - Status: \`SUCCESS\`
    Meaning: Wiz ingested all findings and linked them to assets.
    - Status: \`SKIPPED\`
    Meaning: Wiz parsed findings but could not resolve assets (repository not connected to Wiz).
    - Status: \`FAILURE\`
    Meaning: Schema validation failed or an error occurred.


    `}
</YamlTable>

<Note>
  Even when the system activity status is SUCCESS, Wiz ingests the payload at its own pace. We recommend waiting up to 24 hours for findings to appear in Wiz.
</Note>

## FAQs

<AccordionGroup>
  <Accordion title="Why are findings SKIPPED in Wiz?">
    Wiz links findings only to existing `REPOSITORY_BRANCH` assets in Wiz. Connect the SCM to Wiz so that repositories exist in Wiz's inventory. If the repository is not connected to Wiz, the upload succeeds but Wiz skips the ingestion of the upload.
  </Accordion>

  <Accordion title="Can Wiz data be pulled back into Endor Labs?">
    No, this is a one-way integration that only supports pushing findings from Endor Labs to Wiz.
  </Accordion>

  <Accordion title="How long do findings stay in Wiz?">
    Wiz automatically removes findings that are not refreshed within seven days. To keep your findings current, schedule scans to run regularly. Wiz limits uploads to a maximum of three per branch per day.
  </Accordion>

  <Accordion title="When do findings appear in Wiz after an upload?">
    Even when the system activity status is `SUCCESS`, Wiz ingests the payload at its own pace. We recommend waiting up to 24 hours for findings to reflect in Wiz.
  </Accordion>

  <Accordion title="What happens if a repository has multiple branches?">
    Endor Labs exports findings to Wiz only from the repository’s default branch. Scans on other branches do not export findings to Wiz.
  </Accordion>
</AccordionGroup>
