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

# Container Scanning

> Scan container images for vulnerabilities and secure your deployments.

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

<Note>
  **Important**

  Container scanning now has its own dedicated command: `endorctl container scan`.

  The `endorctl scan --container` commands are deprecated and will be removed after a three-month deprecation period.

  Migrate to the `endorctl container scan` command to ensure continued compatibility. For more details, see [Container scan commands migration guide](/scan/containers/container-migration).
</Note>

Containers help developers create, test, and deploy applications in a consistent environment. Container images include standalone or executable files encompassing files, libraries, and dependencies needed to run a container. They include several open-source software, making them vulnerable to open-source risks. Gaining visibility into container images is essential to identify and prioritize risks or maintain compliance obligations.

Endor Labs container scan detects and reports known vulnerabilities and other risks in:

* **Operating system packages:** Packages installed through the container's base OS package manager.
* **Programming language packages:** packages installed through language-specific package managers.
* **Libraries and dependencies:** Static and dynamic libraries and runtime dependencies required by the application.

Additionally, it generates an **Software Bill of Materials (SBOM)** that details all components, their versions, and associated metadata, providing a complete inventory of the container's contents.

<Tip>
  Upgrade to endorctl version 1.6.734 or higher to ensure accurate container scan results.
</Tip>

## How Endor Labs derives container findings

Endor Labs’ container scanning results rely on OVAL feeds from distributions. OVAL feeds provide accurate and vetted vulnerability data, while excluding disputed or irrelevant entries. OS dependency results are based on data from distribution developers. For language package dependencies, we complement published data with our proprietary research.

Endor Labs fetches the container image from a container registry or loads it from a local file to scan containers. It then proceeds to extract the layers of the container image. It traverses the filesystem of each layer to identify files and directories. It looks for known package manager and metadata files to gather information about installed packages and their versions. It identifies the components and dependencies within the image and presents the findings in CLI and the Endor Labs user interface.

Endor Labs categorizes the severity of vulnerabilities detected in container scans as follows:

* Use the severity assigned by the distribution, if it exists.
* Use the NVD severity if the distribution does not provide the severity.
* Report the vulnerability as `Medium` if there is no severity assigned by the distribution, or the NVD severity is not known or can't be matched.

Endor Labs doesn't report disputed vulnerabilities withdrawn from NVD.

### Discover base images of containers

A container image is often built upon a base image that is a foundational layer including an operating system and other essential components. It's crucial to understand what's in the base image for a thorough security assessment.

You can distinguish the base image related vulnerabilities from the application layer using any of the following methods:

* **Scan Sequence**: First, scan the base image. Then, scan any subsequent images built on that base image to distinguish vulnerabilities specific to the base image from those introduced by the other layers.
* **Docker file label**: Set the label directly in your Dockerfile with a command, for example, `LABEL org.opencontainers.image.base.name="openjdk:17-slim"`.
* **Build time label**: Include the base image label during the build process with the `--label` flag, specifying both the base image and, optionally, its exact version via SHA256 hash. For example:

  ```bash theme={null}
  docker build -t tictactoe:latest --label "org.opencontainers.image.base.name=openjdk@sha256:eddacbc7e24bf8799a4ed3cdcfa50d4b88a323695ad80f317b6629883b2c2a78" .
  ```

<img src="https://mintcdn.com/endorlabs-b4795f4f/rtiT4oc3TglKX_HY/images/scan/containers/base-image.webp?fit=max&auto=format&n=rtiT4oc3TglKX_HY&q=85&s=6dc388eb97a18cde319aa9afa09fac92" alt="Base image" width="1680" height="1230" data-path="images/scan/containers/base-image.webp" />

## Verify access to container registries

If the container image is in a private Docker registry, you must authenticate the container client before the scan.

Here are a few commands to authenticate the container client.

<AccordionGroup>
  <Accordion title="Authenticate to a Docker registry">
    ```bash theme={null}
    docker login <host> -u <user_name> -p <password>
    ```

    [Learn more about Docker authentication](https://docs.docker.com/reference/cli/docker/login/)
  </Accordion>

  <Accordion title="Authenticate to a Podman registry">
    ```bash theme={null}
    podman login -u <user_name> -p <password> <host>
    ```

    [Learn more about Podman authentication](https://docs.podman.io/en/stable/markdown/podman-login.1.html)

    [Endor Labs Podman troubleshooting](https://docs.endorlabs.com/troubleshooting/podman/)
  </Accordion>

  <Accordion title="Authenticate with containerd">
    You must configure the containerd config file to authenticate with the container registry.

    [Learn more about containerd authentication](https://github.com/containerd/containerd/blob/main/docs/cri/registry.md)
  </Accordion>
</AccordionGroup>

## Supported languages and package managers

The dependencies associated with the following list of components are identified in the endorctl scan.

<YamlTable>
  {`


    - OS_/_Language: Alpine
    Package_Manager_Packaging: apk
    Version_Support: 3.20, 3.19, 3.18, 3.17, 3.16, 3.15, 3.14, 3.12, 3.11, 3.10

    - OS_/_Language: Debian
    Package_Manager_Packaging: dpkg
    Version_Support: 8, 9, 10, 11, 12

    - OS_/_Language: Ubuntu
    Package_Manager_Packaging: dpkg
    Version_Support: 18.04, 20.04, 22.04, 24.04, 24.10

    - OS_/_Language: Red Hat
    Package_Manager_Packaging: RPM
    Version_Support: 5, 6, 7, 8, 9

    - OS_/_Language: Fedora
    Package_Manager_Packaging: RPM
    Version_Support: 40, 39

    - OS_/_Language: Amazon Linux
    Package_Manager_Packaging: RPM
    Version_Support: 1, 2, 2022, 2023

    - OS_/_Language: Oracle Linux
    Package_Manager_Packaging: RPM
    Version_Support: 7, 8, 9

    - OS_/_Language: .NET
    Package_Manager_Packaging: \`*.dll\`, \`*.exe\`
    Version_Support:

    - OS_/_Language: Objective-C
    Package_Manager_Packaging: CocoaPods
    Version_Support:

    - OS_/_Language: Go
    Package_Manager_Packaging: Go binaries
    Version_Support:

    - OS_/_Language: Java
    Package_Manager_Packaging: jar, ear, war, native-image
    Version_Support:

    - OS_/_Language: JavaScript
    Package_Manager_Packaging: package.json
    Version_Support:

    - OS_/_Language: PHP
    Package_Manager_Packaging: Composer
    Version_Support:

    - OS_/_Language: Python
    Package_Manager_Packaging: wheel, egg
    Version_Support:

    - OS_/_Language: Ruby
    Package_Manager_Packaging: gem
    Version_Support:

    - OS_/_Language: Rust
    Package_Manager_Packaging: Cargo
    Version_Support:


    `}
</YamlTable>

Endor Labs recognizes only the installed dependencies. Declared but uninstalled dependencies in the container image are not recognized.

## Limitations of container findings

* Scanning Windows containers is not supported.
* Docker file scans are not currently supported.
* Container registry direct integrations are not currently supported.
* Support for scanning binary files inside a container is limited.
* Endor scores are not calculated for findings reported in the container scan.
