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

<AgentInstructions>

## Submitting Feedback

If you encounter incorrect, outdated, or confusing documentation on this page, submit feedback:

POST https://docs.endorlabs.com/feedback

```json
{
  "path": "/integrations/data-exporters/export-to-s3/index",
  "feedback": "Description of the issue"
}
```

Only submit feedback when you have something specific and actionable to report.

</AgentInstructions>

# Export findings to S3

> Learn how to export findings and scan data to an AWS S3 storage bucket using the Endor Labs export framework.

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 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>;
    return processText(text);
  };
  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}>{processBadges(row[col])}</td>)}
          </tr>)}
      </tbody>
    </table>;
};

Export scan data generated by Endor Labs to an AWS S3 storage bucket. This enables long-term data retention for compliance requirements, integration with security information and event management (SIEM) systems, and custom analytics workflows. The export framework supports exporting findings in JSON or SARIF format, allowing flexible integration with your existing toolchain.

Amazon S3 is an object storage service provided by Amazon Web Services (AWS). It offers high durability, availability, and scalability for storing and retrieving any amount of data. S3 integrates with other AWS services and third-party tools, making it ideal for data archival, backup, and analytics workflows.

## Prerequisites

Ensure that you meet the following prerequisites before exporting data to S3:

* An AWS account with permissions to create IAM roles, identity providers, and S3 buckets.
* An S3 bucket to store the exported data. See [Create an S3 bucket](#create-an-s3-bucket).
* An OIDC identity provider configured to allow Endor Labs access. See [Add an OIDC identity provider](#1-add-an-oidc-identity-provider).
* An IAM role with permissions for Endor Labs to write to your S3 bucket. See [Create an IAM role](#2-create-an-iam-role).
* Download and install endorctl. See [Install endorctl](/introduction/getting-started).

## Create an S3 bucket

An S3 bucket is a container for storing objects in Amazon S3. Each bucket has a globally unique name, and you create it in a specific AWS region.

You can create a general purpose S3 bucket or reuse an existing bucket to store the exported data. Disable access control lists (ACLs) on the bucket so IAM policies and bucket policies control access, preventing unintended public access. Refer to [Creating a bucket](https://docs.aws.amazon.com/AmazonS3/latest/userguide/create-bucket-overview.html) for detailed instructions on creating an S3 bucket.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/s3-buckets.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=7fc051e35954059203439afa45b64ee6" alt="S3 buckets" style={{width: '75%'}} width="1680" height="1112" data-path="images/integrations/data-exporters/exporter-s3/s3-buckets.webp" />

#### Configure bucket lifecycle

You can configure S3 lifecycle rules to automatically delete exported data after a specified retention period. Exported objects do not expire unless you configure lifecycle rules.

1. In the AWS management console, navigate to **Amazon S3** > **Buckets**.
2. Select your bucket.
3. Select **Management** and click **Create lifecycle rule**.
4. Enter a **Lifecycle rule name**, for example, `endor-exports-expiry`.
5. Under **Filter type**, select **Limit the scope of this rule using one or more filters** and enter `endor/` as the prefix to apply the rule only to exported data.
6. Under **Lifecycle rule actions**, select **Expire current versions of objects**.
7. Under **Expire current versions of objects**, enter the number of days after which S3 deletes objects.
8. Review the rule and click **Create rule**.

## Configure access for Endor Labs

Endor Labs uses OIDC federation to assume an IAM role in your AWS account to access the S3 bucket. To allow Endor Labs to write to the bucket, configure OIDC and IAM using one of the following methods:

* Use the [CFT template](#create-aws-resources-using-a-cft-template) to create the OIDC identity provider, IAM role, and S3 write policy.
* Use the [AWS Management console](#create-access-through-the-console) to create access by adding the OIDC identity provider and IAM role.

### Create AWS resources using a CFT template

Use an AWS CloudFormation Template (CFT) to create the IAM role and S3 `PutObject` policy for the S3 exporter. The template can create a new OIDC identity provider for Endor Labs or reuse an existing provider in your account.

The following table lists the parameters you can set when deploying the CFT template.

<YamlTable>
  {`


    - Parameter: \`OIDCUrl\`
    Description: Endor Labs OIDC issuer URL.
    - Parameter: \`ExistingOidcProviderArn\`
    Description: Set this to the ARN of your existing OIDC provider for \`api.endorlabs.com\`. The template will reuse it and will not create a new OIDC provider.
    - Parameter: \`OidcAudience\`
    Description: Audience for the OIDC trust policy. Use the same value for \`allowed_audience\` when creating the S3 exporter.
    - Parameter: \`TenantNamespace\`
    Description: Your Endor Labs tenant namespace.
    - Parameter: \`BucketName\`
    Description: Name of the existing S3 bucket that will receive exports.
    - Parameter: \`RoleName\`
    Description: IAM role name that Endor Labs will assume via web identity.
    - Parameter: \`PolicyName\`
    Description: IAM managed policy name for S3 PutObject permission.


    `}
</YamlTable>

1. Create a `.cft` file with the following template.

   You can use the following template and set the parameters according to your OIDC audience, tenant namespace, bucket name, role name, and optionally an existing OIDC provider ARN.

   ```yaml theme={null}
   AWSTemplateFormatVersion: "2010-09-09"
   Description: >
     Endor Labs S3 Exporter - creates IAM OIDC provider (optional), role, and minimal S3 PutObject policy.
   Parameters:
     OIDCUrl:
       Type: String
       Default: "https://api.endorlabs.com"
       Description: "Endor Labs OIDC issuer URL."
     ExistingOidcProviderArn:
       Type: String
       Default: ""
       Description: >
         Optional. If your AWS account already has an OIDC provider for https://api.endorlabs.com,
         set this to its ARN (for example, arn:aws:iam::<ACCOUNT_ID>:oidc-provider/api.endorlabs.com).
         When set, this template will NOT create a new OIDC provider. It will reuse the existing provider
         and ensure the OidcAudience is present in its ClientIdList.
     OidcAudience:
       Type: String
       Default: "s3-exporter"
       Description: "Specify the audience name to use in the OIDC trust policy. Set the same value in allowed_audience while creating the Endor exporter configuration."
     TenantNamespace:
       Type: String
       Description: "Root Endor Labs tenant namespace (for example, acme-corp)."
     BucketName:
       Type: String
       Description: "Existing S3 bucket name to receive exports."
     RoleName:
       Type: String
       Default: "EndorS3ExporterRole"
       Description: "IAM role name Endor will assume via web identity."
     PolicyName:
       Type: String
       Default: "EndorS3ExporterPolicy"
       Description: "IAM managed policy name for S3 PutObject permission."
   Conditions:
     CreateOidcProvider: !Equals [!Ref ExistingOidcProviderArn, ""]
     UseExistingOidcProvider: !Not [!Equals [!Ref ExistingOidcProviderArn, ""]]
   Resources:
     EndorOidcProvider:
       Type: AWS::IAM::OIDCProvider
       DeletionPolicy: Delete
       UpdateReplacePolicy: Delete
       Condition: CreateOidcProvider
       Properties:
         Url: !Ref OIDCUrl
         ClientIdList:
           - !Ref OidcAudience
     EndorS3PutObjectPolicy:
       Type: AWS::IAM::ManagedPolicy
       DeletionPolicy: Delete
       UpdateReplacePolicy: Delete
       Properties:
         ManagedPolicyName: !Ref PolicyName
         PolicyDocument:
           Version: "2012-10-17"
           Statement:
             - Sid: PutObjectToBucket
               Effect: Allow
               Action:
                 - s3:PutObject
               Resource: !Sub "arn:${AWS::Partition}:s3:::${BucketName}/*"
     EndorS3ExporterRole:
       Type: AWS::IAM::Role
       DeletionPolicy: Delete
       UpdateReplacePolicy: Delete
       Properties:
         RoleName: !Ref RoleName
         AssumeRolePolicyDocument:
           Version: "2012-10-17"
           Statement:
             - Sid: EndorWebIdentity
               Effect: Allow
               Principal:
                 Federated: !If
                   - CreateOidcProvider
                   - !Ref EndorOidcProvider
                   - !Ref ExistingOidcProviderArn
               Action: sts:AssumeRoleWithWebIdentity
               Condition:
                 StringEquals:
                   "api.endorlabs.com:aud": !Ref OidcAudience
                 StringLike:
                   "api.endorlabs.com:sub":
                     - !Sub "${TenantNamespace}/*"
                     - !Sub "${TenantNamespace}.*/*"
         ManagedPolicyArns:
           - !Ref EndorS3PutObjectPolicy
   Outputs:
     OidcProviderArn:
       Description: "OIDC provider ARN."
       Value: !If
         - CreateOidcProvider
         - !Ref EndorOidcProvider
         - !Ref ExistingOidcProviderArn
     RoleArn:
       Description: "Role ARN to set as assume_role_arn in Endor exporter config."
       Value: !GetAtt EndorS3ExporterRole.Arn
     OidcAudienceOut:
       Description: "Audience to set as allowed_audience in Endor exporter config."
       Value: !Ref OidcAudience
   ```

2. Save this file with an appropriate name such as `endorlabs-s3-export.cft`.

3. Sign into AWS CloudFormation and search for **Stacks**.

4. Click **Create Stack** and select **With new resources**.

5. From **Template source**, select **Upload a template file**.

6. Click **Choose file**, select the file you saved, and click **Next**.

7. In **Specify stack details**, enter a **Stack name**, verify the **Parameters** you entered in the script and click **Next**.

8. Select the acknowledgement from **Configure stack options** and click **Next**.

9. From **Review and Create**, review the details and click **Submit**.

Check the progress of the creation of your resources from **Stacks**. After AWS creates the stack, you can see the status as **CREATE\_COMPLETE**.

### Create access through the Console

Create the OIDC identity provider and IAM role manually in the AWS Management Console.

#### 1. Add an OIDC identity provider

OpenID Connect (OIDC) federation allows Endor Labs to access AWS resources without requiring long-lived credentials. This reduces the risk of credential exposure and simplifies secret rotation.

1. In the AWS management console, navigate to **IAM** > **Access Management** > **Identity providers**.
2. Click **Add provider**.
3. Under **Provider details**, select **OpenID Connect**.
4. For **Provider URL**, enter `https://api.endorlabs.com`.
5. For **Audience**, specify a unique identifier to validate incoming OIDC tokens from Endor Labs.
6. Optionally, add tags to help identify the provider.
7. Click **Add provider**.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/create-identity-provider-for-s3.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=b6009360c19a93a8f3a7bd095dd849d8" alt="Create identity provider" style={{width: '75%'}} width="2014" height="1218" data-path="images/integrations/data-exporters/exporter-s3/create-identity-provider-for-s3.webp" />

#### 2. Create an IAM role

Create an IAM role that Endor Labs can assume to write to your S3 bucket. This involves:

1. [Create a permissions policy](#create-a-permissions-policy): Define the S3 write permissions.
2. [Create an IAM role](#create-the-iam-role): Create a role with OIDC trust and attach the policy.

#### Create a permissions policy

1. In the AWS management console, navigate to **IAM** > **Access Management** > **Policies**.
2. Click **Create policy**.
3. Under **Specify permissions**, toggle the **Policy editor** to **JSON**.
4. Enter the following policy:

   ```json theme={null}
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Effect": "Allow",
         "Action": [
           "s3:PutObject"
         ],
         "Resource": "arn:aws:s3:::<your-bucket-name>/*"
       }
     ]
   }
   ```

   Replace `<your-bucket-name>` with the name of your S3 bucket.
5. Click **Next**.
6. Under **Review and create**, enter a **Policy name**. For example, `EndorLabsS3ExportPolicy`.
7. Review the **Permissions defined in this policy section** to confirm that this policy lists the expected Amazon S3 write actions.
8. Optionally, add a description and tags to your policy.
9. Click **Create policy**.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/review-and-create-iam-policy.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=3affcdeb1adf648fce286402283f431e" alt="Policy permissions" style={{width: '80%'}} width="2270" height="1872" data-path="images/integrations/data-exporters/exporter-s3/review-and-create-iam-policy.webp" />

#### Create an IAM role

1. In the AWS management console, navigate to **IAM** > **Access Management** > **Roles**.
2. Click **Create role**.
3. Under **Select trusted entity**, select **Custom trust policy**.
4. Enter the following trust policy:
   ```json theme={null}
   {
     "Version": "2012-10-17",
     "Statement": [
       {
         "Sid": "EndorWebIdentity",
         "Effect": "Allow",
         "Principal": {
           "Federated": "arn:aws:iam::<aws-account-id>:oidc-provider/api.endorlabs.com"
         },
         "Action": "sts:AssumeRoleWithWebIdentity",
         "Condition": {
           "StringEquals": {
             "api.endorlabs.com:aud": "<oidc-audience>"
           },
           "StringLike": {
             "api.endorlabs.com:sub": [ "<your-namespace>/*", "<your-namespace>.*" ]
           }
         }
       }
     ]
   }
   ```
   Replace the placeholders with your values:
   * `<aws-account-id>`: Your AWS account ID
   * `<oidc-audience>`: The audience value you configured in the OIDC provider
   * `<your-namespace>`: Your Endor Labs namespace

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/create-iam-role.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=4e1b3a332d481ae9d6174ab251a073d4" alt="Create IAM role" style={{width: '75%'}} width="2322" height="2055" data-path="images/integrations/data-exporters/exporter-s3/create-iam-role.webp" />

5. Click **Next**.
6. Under **Add permissions**, search for and select the IAM policy you created.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/iam-role-permissions.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=df50735625f9682171c87a0dd2054026" alt="IAM role permissions" style={{width: '75%'}} width="1772" height="1238" data-path="images/integrations/data-exporters/exporter-s3/iam-role-permissions.webp" />

7. Click **Next**.
8. Under **Name, review, and create**, enter a **Role name** for the S3 exporter role. For example, `EndorLabsS3ExporterRole`.

<img src="https://mintcdn.com/endorlabs-b4795f4f/dHzwUrp_QbpzV9uv/images/integrations/data-exporters/exporter-s3/iam-role-name.webp?fit=max&auto=format&n=dHzwUrp_QbpzV9uv&q=85&s=78fe9835d6db5ce3982651c9f1537239" alt="IAM role name" style={{width: '75%'}} width="1746" height="2353" data-path="images/integrations/data-exporters/exporter-s3/iam-role-name.webp" />

9. Optionally, add tags to help identify the role.
10. Click **Create role**.

## Create an S3 exporter

Create an S3 exporter using 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: \`<your-bucket-name>\`
    Description: The name of your S3 bucket
    - Parameter: \`<aws-region>\`
    Description: The AWS region that hosts your bucket, for example \`us-east-1\`. Refer to [AWS regions](https://docs.aws.amazon.com/global-infrastructure/latest/regions/aws-regions.html) for a list of region codes.
    - Parameter: \`<iam-role-arn>\`
    Description: The ARN of the IAM role you created
    - Parameter: \`<oidc-audience>\`
    Description: The audience value that you configured in the OIDC provider


    `}
</YamlTable>

Run the following command to create an S3 exporter.

```bash expandable theme={null}
endorctl api create \
  --namespace=<namespace> \
  --resource=Exporter \
  --data '{
    "meta": {
      "name": "<exporter-name>"
    },
    "propagate": true,
    "spec": {
      "exporter_type": "EXPORTER_TYPE_S3",
      "s3_config": {
        "bucket_name": "<your-bucket-name>",
        "region": "<aws-region>",
        "assume_role_arn": "<iam-role-arn>",
        "allowed_audience": "<oidc-audience>"
      },
      "message_type_configs": [
        {
          "message_type": "MESSAGE_TYPE_FINDING",
          "message_export_format": "MESSAGE_EXPORT_FORMAT_JSON"
        }
      ]
    }
  }'
```

For example, to create an S3 exporter named `s3-findings-exporter` in the namespace `doe.deer` that exports findings in JSON format, run the following command.

```bash expandable theme={null}
endorctl api create \
  --namespace=doe.deer \
  --resource=Exporter \
  --data '{
    "meta": {
      "name": "s3-findings-exporter"
    },
    "propagate": true,
    "spec": {
      "exporter_type": "EXPORTER_TYPE_S3",
      "s3_config": {
        "bucket_name": "my-endorlabs-exports",
        "region": "us-west-2",
        "assume_role_arn": "arn:aws:iam::123456789012:role/EndorLabsS3ExportRole",
        "allowed_audience": "s3-exporter"
      },
      "message_type_configs": [
        {
          "message_type": "MESSAGE_TYPE_FINDING",
          "message_export_format": "MESSAGE_EXPORT_FORMAT_JSON"
        }
      ]
    }
  }'
```

## Configure scan profile to use the S3 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 **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**.

## Scan projects to export data

After configuration, subsequent scans automatically export data to your S3 bucket. You can trigger a scan immediately using the rescan feature. See [Rescan projects](/setup-deployment/scm-integrations/github-app-pro/re-scan-projects) for more information.

To validate that the S3 exporter ran successfully for a scan:

1. Select **Projects** from the left sidebar and select the project associated with your exporter.
2. Select **Scan History** and select a record to view its information.
3. Select **Logs** to view the scan log and set the log level to **All**

   The following message confirms that the S3 export is successful.
   `INFO: Successfully completed S3 export`

### Exported file structure

Endor Labs exports data to S3 using a hierarchical folder structure:

```text theme={null}
endor/
└── <exporter-uuid>-<exporter-name>/
    └── <namespace>/
        └── <project-uuid>-<project-name>/
            └── <scan-type>/
                └── <ref-or-pr>/
                    └── <timestamp>_<scan-uuid>.zip
```

The following table explains each path segment:

<YamlTable>
  {`


    - Level: Root
    Example: \`endor/\`
    Description: Fixed prefix for all Endor exports
    - Level: Exporter
    Example: \`abc123-prod-exporter/\`
    Description: \`<exporter_uuid>-<exporter_name>\` - unique per exporter
    - Level: Namespace
    Example: \`acme-corp/\`
    Description: Your Endor Labs namespace
    - Level: Project
    Example: \`def456-my-service/\`
    Description: \`<project-uuid>-<project-name>\`
    - Level: Scan Type
    Example: \`schedule/\` or \`pr/\`
    Description: Type of scan that triggered export
    - Level: Ref or PR Number
    Example: \`<branch-name>/\` or \`<pr-id>\`
    Description: Name of the branch or PR number
    - Level: File
    Example: \`20251215T143025Z_xyz789.zip\`
    Description: \`<timestamp>_<scan-uuid>.zip\`


    `}
</YamlTable>

#### Example file path

```text theme={null}
my-bucket/endor/abc123-prod-exporter/acme-corp/6efgh-pythonrepo/schedule/main/20251215T143025Z_xyz789.zip
```

## Manage the S3 exporter

You can list, update, and delete S3 exporters using the Endor Labs API.

<AccordionGroup>
  <Accordion title="List exporters">
    Run the following command to list all exporters in your namespace.

    ```bash theme={null}
    endorctl api list --namespace=<namespace> --resource=Exporter
    ```
  </Accordion>

  <Accordion title="Update an exporter">
    Run the following command to update an existing exporter. Use the `--field-mask` parameter to specify the fields to update.

    ```bash theme={null}
    endorctl api update \
      --namespace=<namespace> \
      --resource=Exporter \
      --name=<exporter-name> \
      --field-mask "spec.s3_config.region" \
      --data '{
        "spec": {
          "s3_config": {
            "region": "us-west-2"
          }
        }
      }'
    ```
  </Accordion>

  <Accordion title="Delete an exporter">
    <Note>
      You must dissociate the exporter from any linked scan profiles before deletion.
    </Note>

    Run the following command to delete an exporter.

    ```bash theme={null}
    endorctl api delete --namespace=<namespace> --resource=Exporter --name=<exporter-name>
    ```
  </Accordion>
</AccordionGroup>
