> ## Documentation Index
> Fetch the complete documentation index at: https://docs.hiddenlayer.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Scan models for security risks

> Use the HiddenLayer Python SDK to scan model files and folders from local disk, Hugging Face, AWS S3, and Azure Blob Storage, and read the scan report.

Model scanning is part of HiddenLayer [AI Supply Chain Security](/docs/products/supply-chain/overview). A scan inspects model files for security risks, such as malicious code embedded in a serialized model, before you load or deploy them. The SDK uploads your model to HiddenLayer (or asks HiddenLayer to fetch it), waits for the scan to finish, and returns a scan report.

Use model scanning when you:

* Download a third-party model and want to check it before loading it.
* Want to block risky models in a CI/CD pipeline.
* Need a record of scan results for models stored in Hugging Face, AWS S3, or Azure Blob Storage.

## Before you begin

* Install the SDK and configure credentials. See [Installation](/docs/integrations/sdk_python/installation) and [Authentication and configuration](/docs/integrations/sdk_python/authentication).
* Set `HIDDENLAYER_CLIENT_ID` and `HIDDENLAYER_CLIENT_SECRET` in your environment. Alternatively, set `HIDDENLAYER_TOKEN` to a bearer token. Every example on this page reads credentials from these variables. None of them hard-code secrets.
* Your tenant must have AI Supply Chain Security enabled, and your API key needs permission to use it.

Some helpers need extra packages. These are not installed with the SDK, and they are not SDK extras. Install them separately only if you use the matching helper:

| Helper | Extra packages |
| - | - |
| `scan_huggingface_model` | `huggingface_hub` |
| `scan_s3_model` | `boto3` |
| `scan_azure_blob_model` | `azure-identity` and `azure-storage-blob` |

## Understand the scan report

Every scan method returns a `ScanReport`. A `ScanReport` is a Pydantic model, which means you read its fields as attributes, such as `report.status`. Import the type with `from hiddenlayer.types.scans import ScanReport` if you want type hints.

The fields you use most often are:

| Field | Description |
| - | - |
| `scan_id` | Unique ID of the scan. Use it to fetch results later. |
| `status` | `pending`, `running`, `done`, `failed`, or `canceled`. |
| `summary.highest_severity` | Highest severity across all detections: `critical`, `high`, `medium`, `low`, `none`, or `unknown`. |
| `summary.detection_count` | Total number of detections. |
| `summary.file_count` | Number of files scanned. |
| `summary.files_with_detections_count` | Number of files that have at least one detection. |
| `summary.files_failed_to_scan` | Number of files that could not be scanned. |
| `summary.detection_categories` | List of detection categories found. |
| `inventory` | Model details, such as `model_name`, `model_version`, `model_id`, and `origin`. |
| `file_results` | One entry per file, with `file_location`, `status`, `details` (for example `details.file_type` and `details.sha256`), `detections`, and `file_error`. |
| `compliance.status` | `COMPLIANT` or `NONCOMPLIANT`, when compliance rules apply. |

Each item in `file_results[].detections` includes `severity` (`critical`, `high`, `medium`, or `low`), `category`, `description`, `risk` (`MALICIOUS` or `SUSPICIOUS`), `rule_id`, and mapping fields such as `cve`, `cwe`, `mitre_atlas`, and `owasp`.

> [!WARNING]
>
> `ScanReport` also has top-level `detection_count`, `file_count`, `files_with_detections_count`, `severity`, and `detection_categories` fields. These are deprecated. Read the same values from `report.summary` instead. Some older examples, including `examples/demo.py` in the SDK repository, still use the deprecated fields.

> [!NOTE]
>
> A scan that fails is returned, not raised as an exception. Always check `report.status` before you trust the summary. A `failed` scan may have zero detections only because the scan did not complete.

## Choose a scan method

| Method | Use it when your model is | How the model reaches HiddenLayer |
| - | - | - |
| `client.model_scanner.scan_file` | A single file on local disk | The SDK uploads the file |
| `client.model_scanner.scan_folder` | A folder of files on local disk | The SDK uploads each file |
| `client.model_scanner.scan_huggingface_model` | A Hugging Face repository | The SDK downloads it locally, then uploads it |
| `client.model_scanner.scan_s3_model` | A single file in AWS S3 | The SDK downloads it locally, then uploads it |
| `client.model_scanner.scan_azure_blob_model` | A single blob in Azure Blob Storage | The SDK downloads it locally, then uploads it |
| `client.community_scanner.community_scan` | At a remote location HiddenLayer can access | HiddenLayer fetches it. Nothing is downloaded to your machine |

All methods accept `wait_for_results` (default `True`). When it is `True`, the method blocks until the scan finishes. See [Start a scan without waiting](#start-a-scan-without-waiting) for the alternative.

## Scan a local file

Use `scan_file` to scan one model file, such as a `.pkl`, `.pt`, `.h5`, or `.safetensors` file. The SDK uploads the file in parts, tells HiddenLayer the upload is complete, and then polls until the scan finishes.

The following script scans a file, prints the results, and exits with a non-zero code if the scan fails or finds a `critical` or `high` detection. You can run it as a CI gate: most CI systems fail the job when a step exits with a non-zero code.

```python scan_file.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

MODEL_PATH = "models/model.pkl"  # Path to the file you want to scan
BLOCKING_SEVERITIES = {"critical", "high"}


def main() -> int:
    # The client reads HIDDENLAYER_CLIENT_ID and HIDDENLAYER_CLIENT_SECRET
    # (or HIDDENLAYER_TOKEN) from the environment.
    with HiddenLayer() as client:
        try:
            report = client.model_scanner.scan_file(
                model_name="my-model",
                model_version="1",
                model_path=MODEL_PATH,
            )
        except FileNotFoundError:
            print(f"Model file not found: {MODEL_PATH}", file=sys.stderr)
            return 2
        except hiddenlayer.AuthenticationError:
            print("Authentication failed. Check your client ID, client secret, and environment.", file=sys.stderr)
            return 2
        except hiddenlayer.APIStatusError as e:
            print(f"HiddenLayer API error {e.status_code}: {e.message}", file=sys.stderr)
            return 2
        except hiddenlayer.APIConnectionError as e:
            print(f"Could not reach HiddenLayer: {e}", file=sys.stderr)
            return 2

    print(f"Scan ID:          {report.scan_id}")
    print(f"Status:           {report.status}")
    print(f"Highest severity: {report.summary.highest_severity}")
    print(f"Detections:       {report.summary.detection_count}")

    for file_result in report.file_results or []:
        print(f"\nFile: {file_result.file_location} ({file_result.status})")
        for detection in file_result.detections:
            print(f"  [{detection.severity}] {detection.category}: {detection.description}")

    if report.status != "done":
        print(f"Scan did not complete (status: {report.status}).", file=sys.stderr)
        return 2

    if report.summary.highest_severity in BLOCKING_SEVERITIES:
        print("Blocking: model has critical or high severity detections.", file=sys.stderr)
        return 1

    return 0


if __name__ == "__main__":
    sys.exit(main())
```

**Expected result:** the script prints the scan ID, status, highest severity, and detection count. For each file it prints any detections. For a model with no detections, the status is `done`, the highest severity is typically `none`, and the script exits with code `0`.

The `model_name` and `model_version` values identify the model in the HiddenLayer Console. `model_version` defaults to `"1"`.

## Scan a folder

Use `scan_folder` when a model consists of several files, such as a weights file plus configuration and tokenizer files. The SDK walks the folder recursively and uploads every file that passes the filters.

You can filter files with [fnmatch](https://docs.python.org/3/library/fnmatch.html) patterns (shell-style wildcards such as `*.bin`):

* `allow_file_patterns`: if set, only files that match at least one pattern are scanned.
* `ignore_file_patterns`: files that match any pattern are skipped.

The SDK always excludes these patterns, in addition to any you pass: `*.txt`, `*.md`, `*.lock`, `.gitattributes`, and `.git` directories.

```python scan_folder.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

client = HiddenLayer()

try:
    report = client.model_scanner.scan_folder(
        model_name="my-folder-model",
        model_version="1",
        path="models/my-model",
        allow_file_patterns=["*.bin", "*.pt", "*.safetensors", "*.json"],
        ignore_file_patterns=["*.png"],
    )
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
except hiddenlayer.APIConnectionError as e:
    sys.exit(f"Could not reach HiddenLayer: {e}")
finally:
    client.close()

print(f"{report.status}: {report.summary.file_count} files, "
      f"{report.summary.detection_count} detections, "
      f"highest severity {report.summary.highest_severity}")
```

**Expected result:** one report that covers every uploaded file, with one entry per file in `report.file_results`.

> [!NOTE]
>
> If HiddenLayer rejects a file as a duplicate within the same scan, the SDK logs a warning and skips that file instead of failing the whole scan.

## Scan a Hugging Face model

Use `scan_huggingface_model` to scan a model repository on Hugging Face. The SDK downloads the repository with `huggingface_hub`, then scans the downloaded folder.

**Extra prerequisite:** `pip install huggingface_hub`.

For private or gated repositories, provide a Hugging Face access token with `hf_token`:

* Pass a string to use that token. The example reads it from an `HF_TOKEN` environment variable that you set yourself.
* Pass `True` to read the token saved in your local Hugging Face configuration (for example, after you log in with the Hugging Face CLI).

```python scan_huggingface.py theme={null}
import os
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

client = HiddenLayer()

try:
    report = client.model_scanner.scan_huggingface_model(
        repo_id="openai-community/gpt2",
        # Optional: branch, tag, or commit hash. Also used as the model version.
        # revision="main",
        allow_file_patterns=["*.safetensors", "*.bin", "*.json"],
        # Use HF_TOKEN if set; otherwise read the token saved by the Hugging Face CLI.
        hf_token=os.environ.get("HF_TOKEN") or True,
    )
except ImportError as e:
    sys.exit(f"{e} Run: pip install huggingface_hub")
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
except hiddenlayer.APIConnectionError as e:
    sys.exit(f"Could not reach HiddenLayer: {e}")
finally:
    client.close()

print(f"{report.inventory.model_name}: {report.status}, "
      f"highest severity {report.summary.highest_severity}")
```

If you omit `model_name`, the SDK uses `repo_id` as the model name. If you omit `revision`, the model version is `"1"`. Other options are `local_dir` (download location), `ignore_file_patterns`, and `force_download`.

**Expected result:** a report for the downloaded repository with `report.inventory.origin` set to `Hugging Face`.

## Scan a model in AWS S3

Use `scan_s3_model` to scan a single file stored in S3. The SDK downloads the file with `boto3` and then calls `scan_file`.

**Extra prerequisite:** `pip install boto3`.

By default the SDK creates `boto3.client("s3")`, which uses the standard AWS credential chain. That chain checks sources such as the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` environment variables, your AWS config files, and attached IAM roles. Pass your own client with `s3_client` if you need a specific profile or region.

```python scan_s3.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

client = HiddenLayer()

try:
    report = client.model_scanner.scan_s3_model(
        model_name="my-s3-model",
        bucket="my-model-bucket",
        key="models/v1/model.pkl",
    )
except ImportError as e:
    sys.exit(f"{e} Run: pip install boto3")
except RuntimeError as e:
    # Raised when the download from S3 fails, for example due to missing AWS permissions.
    sys.exit(str(e))
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
except hiddenlayer.APIConnectionError as e:
    sys.exit(f"Could not reach HiddenLayer: {e}")
finally:
    client.close()

print(f"{report.status}: highest severity {report.summary.highest_severity}")
```

**Expected result:** a report with `report.inventory.origin` set to `S3`.

## Scan a model in Azure Blob Storage

Use `scan_azure_blob_model` to scan a single blob. The SDK downloads the blob with the Azure Storage SDK and then calls `scan_file`.

**Extra prerequisite:** `pip install azure-identity azure-storage-blob`.

By default the SDK authenticates with `DefaultAzureCredential`, which tries several Azure sign-in methods, such as environment variables, managed identity, and the Azure CLI. To use a shared access signature (SAS) token instead, pass it as `credential`. Read it from an environment variable rather than writing it in your code.

**DefaultAzureCredential**

```python scan_azure_default.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

client = HiddenLayer()

try:
    report = client.model_scanner.scan_azure_blob_model(
        model_name="my-azure-model",
        account_url="https://mystorageaccount.blob.core.windows.net",
        container="models",
        blob="v1/model.pkl",
    )
except ImportError as e:
    sys.exit(f"{e} Run: pip install azure-identity azure-storage-blob")
except RuntimeError as e:
    # Raised when the blob download fails.
    sys.exit(str(e))
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
finally:
    client.close()

print(f"{report.status}: highest severity {report.summary.highest_severity}")
```

**SAS token**

```python scan_azure_sas.py theme={null}
import os
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer

client = HiddenLayer()

try:
    report = client.model_scanner.scan_azure_blob_model(
        model_name="my-azure-model",
        account_url="https://mystorageaccount.blob.core.windows.net",
        container="models",
        blob="v1/model.pkl",
        # AZURE_STORAGE_SAS_TOKEN is a variable you define; it is not read by the SDK.
        credential=os.environ["AZURE_STORAGE_SAS_TOKEN"],
    )
except KeyError:
    sys.exit("Set the AZURE_STORAGE_SAS_TOKEN environment variable.")
except RuntimeError as e:
    sys.exit(str(e))
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
finally:
    client.close()

print(f"{report.status}: highest severity {report.summary.highest_severity}")
```

**Expected result:** a report with `report.inventory.origin` set to `Azure Blob Storage`.

### Temporary files for downloaded models

> [!WARNING]
>
> The S3 and Azure helpers always download to `/tmp/<file name>`. The Hugging Face helper downloads to `/tmp/<repo_id>` unless you set `local_dir`. Keep these points in mind:
>
> * The SDK does not delete downloaded files after the scan. Remove them yourself if disk space or data retention matters.
> * Because the paths are hard-coded, the S3 and Azure helpers may not work on Windows. This needs confirmation. On Windows, consider downloading the file yourself and calling `scan_file`, or setting `local_dir` for Hugging Face.

## Scan a remote model with the community scanner

Use `client.community_scanner.community_scan` when HiddenLayer can reach the model directly, for example through a presigned URL or a public Hugging Face repository. HiddenLayer fetches the model itself, so nothing is downloaded to or uploaded from your machine.

Set `model_source` to one of the `CommunityScanSource` constants:

| Constant | Source type |
| - | - |
| `CommunityScanSource.HUGGING_FACE` | Hugging Face repository. Set `model_path` to the repository ID. |
| `CommunityScanSource.AWS_PRESIGNED` | AWS S3 presigned URL |
| `CommunityScanSource.AWS_IAM_ROLE` | AWS S3 through an IAM role |
| `CommunityScanSource.AZURE_BLOB_SAS` | Azure Blob Storage with a SAS URL |
| `CommunityScanSource.AZURE_BLOB_AD` | Azure Blob Storage with Microsoft Entra ID (Azure AD) |
| `CommunityScanSource.GOOGLE_SIGNED` | Google Cloud Storage signed URL |
| `CommunityScanSource.GOOGLE_OAUTH` | Google Cloud Storage with OAuth |
| `CommunityScanSource.LOCAL` | Local source |

> [!NOTE]
>
> The SDK defines these constants, but this page does not document what access setup each source type requires on the HiddenLayer side (for example, which IAM role HiddenLayer assumes). This needs confirmation. The `HUGGING_FACE` example below follows the SDK's own usage.

```python community_scan.py theme={null}
import sys

import hiddenlayer
from hiddenlayer import HiddenLayer
from hiddenlayer.lib import CommunityScanSource

client = HiddenLayer()

try:
    report = client.community_scanner.community_scan(
        model_name="bert-tiny-torch-vuln",
        model_path="drhyrum/bert-tiny-torch-vuln",  # Hugging Face repository ID
        model_source=CommunityScanSource.HUGGING_FACE,
        model_version="main",  # Default is "main"
    )
except ValueError as e:
    # Raised if the API does not return a scan ID.
    sys.exit(str(e))
except hiddenlayer.APIStatusError as e:
    sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
except hiddenlayer.APIConnectionError as e:
    sys.exit(f"Could not reach HiddenLayer: {e}")
finally:
    client.close()

print(f"{report.scan_id}: {report.status}, "
      f"{report.summary.detection_count} detections, "
      f"highest severity {report.summary.highest_severity}")
```

**Expected result:** a report for the remote model. The repository in this example is intentionally vulnerable, so expect detections.

## Start a scan without waiting

By default, scan methods block until the scan finishes. Pass `wait_for_results=False` to return as soon as the scan starts. This is useful in web services or pipelines where you do not want to hold a process open.

With `wait_for_results=False`:

* For upload-based methods, the upload itself still finishes before the method returns. Only the wait for the analysis is skipped.
* The returned report is a point-in-time snapshot. Its `status` is often `pending` or `running`, and `file_results` may be incomplete.
* Save `report.scan_id`, then call `scan_result(scan_id=...)` later to fetch an updated report. Both `client.model_scanner` and `client.community_scanner` provide `scan_result`.

```python start_then_fetch.py theme={null}
from hiddenlayer import HiddenLayer

with HiddenLayer() as client:
    started = client.model_scanner.scan_file(
        model_name="my-model",
        model_path="models/model.pkl",
        wait_for_results=False,
    )
    print(f"Started scan {started.scan_id} (status: {started.status})")

    # Later, possibly in another process:
    report = client.model_scanner.scan_result(scan_id=started.scan_id)
    print(f"Status now: {report.status}")
```

If the new scan is not visible yet, the SDK retries a few times. It raises `hiddenlayer.NotFoundError` if the scan still cannot be found.

## Wait with a time limit

When `wait_for_results=True`, the SDK polls the scan status with exponential backoff (up to 30 seconds between checks) until the scan is `done`, `failed`, or `canceled`. **There is no overall timeout.** If a scan stays in `pending` or `running`, your program waits indefinitely.

In CI/CD or other unattended environments, start the scan with `wait_for_results=False` and poll yourself with a deadline. `client.scans.results.retrieve_summary(scan_id)` is a lightweight call that returns the status and summary without the per-file results.

```python scan_with_timeout.py theme={null}
import sys
import time

import hiddenlayer
from hiddenlayer import HiddenLayer

TIMEOUT_SECONDS = 15 * 60
POLL_INTERVAL_SECONDS = 10
TERMINAL_STATUSES = {"done", "failed", "canceled"}

with HiddenLayer() as client:
    try:
        started = client.model_scanner.scan_file(
            model_name="my-model",
            model_path="models/model.pkl",
            wait_for_results=False,
        )
        scan_id = started.scan_id
        deadline = time.monotonic() + TIMEOUT_SECONDS

        while True:
            summary = client.scans.results.retrieve_summary(scan_id)
            if summary.status in TERMINAL_STATUSES:
                break
            if time.monotonic() > deadline:
                sys.exit(f"Scan {scan_id} did not finish within {TIMEOUT_SECONDS} seconds "
                         f"(last status: {summary.status}).")
            time.sleep(POLL_INTERVAL_SECONDS)

        # Fetch the full report, including per-file results.
        report = client.model_scanner.scan_result(scan_id=scan_id)
    except hiddenlayer.APIStatusError as e:
        sys.exit(f"HiddenLayer API error {e.status_code}: {e.message}")
    except hiddenlayer.APIConnectionError as e:
        sys.exit(f"Could not reach HiddenLayer: {e}")

print(f"{report.status}: highest severity {report.summary.highest_severity}")
```

**Expected result:** the script either prints the final status and highest severity, or exits with a timeout message that includes the scan ID so you can check it later.

For lower-level scan APIs, such as listing scan jobs, paging through file results, and downloading results in SARIF format (Static Analysis Results Interchange Format, a JSON format that many code-scanning tools accept), see [API usage examples](/docs/integrations/sdk_python/api_examples).

## Common errors and fixes

> **ImportError: Python package huggingface\_hub (or boto3, azure-identity, azure-storage-blob) is not installed.**
>
> The helper you called needs a third-party package that the SDK does not install. Install the package listed in [Before you begin](#before-you-begin), for example `pip install boto3`.

> **AuthenticationError (401)**
>
> HiddenLayer rejected your credentials. Check that `HIDDENLAYER_CLIENT_ID` and `HIDDENLAYER_CLIENT_SECRET` are set in the same shell or process that runs the script. Also check that the environment matches your account's region (`prod-us` or `prod-eu`). See [Authentication and configuration](/docs/integrations/sdk_python/authentication).

> **PermissionDeniedError (403)**
>
> Your credentials are valid but lack access to model scanning. Ask a Console administrator to check the API key's permissions.

> **RuntimeError: Couldn't download model ...**
>
> The S3 or Azure download failed before the scan started. The message includes the underlying error. Check the bucket, key, container, and blob names, and confirm that your AWS or Azure credentials can read the object.

> **FileNotFoundError or the script cannot find the model**
>
> `model_path` and `path` are resolved relative to the current working directory. Use an absolute path, or run the script from the expected directory.

> **The scan returns with status failed**
>
> Failed scans are returned, not raised. Inspect `file_error` on each entry in `report.file_results` for scanner error messages, and check `report.summary.files_failed_to_scan`.

> **The script hangs while waiting for results**
>
> `wait_for_results=True` has no overall timeout. Use the pattern in [Wait with a time limit](#wait-with-a-time-limit).

> **NotFoundError right after starting a scan**
>
> With `wait_for_results=False`, the SDK retries a few times while a new scan becomes available. If it still raises `NotFoundError`, wait briefly and call `scan_result(scan_id=...)` again. Also confirm you are using the same environment (region) that started the scan.

For more on exceptions and retries, see [Error handling](/docs/integrations/sdk_python/error_handling) and [Troubleshooting](/docs/integrations/sdk_python/troubleshooting).
