Files
prowler/notebooks/image_provider_api_test_output.ipynb
T
Andoni A. 348808b37f feat(image): add image provider scan support to Scan class and API layer
- Add image provider handling in Scan class alongside IaC (skip check loading, run Trivy directly)
- Add image provider API models, serializers, and connection check logic
- Point API SDK dependency to this feature branch
- Add test notebook for image provider API workflow
2026-02-18 08:59:13 +01:00

52 KiB

Image Provider API Testing

This notebook tests the Image Provider API endpoints added in PROWLER-940.

Prerequisites:

  • API running on localhost:8080 (make build-and-run-api-dev)
  • Dev fixtures loaded (automatic on startup)

Workflow tested:

  1. Authentication
  2. Create image provider
  3. Create provider secret (multiple auth methods)
  4. Test provider connection
  5. Trigger a scan
  6. Monitor scan progress
  7. Retrieve findings
  8. Cleanup

Setup

In [1]:
import json
import time

import requests

BASE_URL = "http://localhost:8080/api/v1"
CONTENT_TYPE = "application/vnd.api+json"
HEADERS = {"Content-Type": CONTENT_TYPE}

# Dev user credentials
EMAIL = "dev@prowler.com"
PASSWORD = "Thisisapassword123@"


def pp(response):
    """Pretty-print a response with status code."""
    print(f"HTTP {response.status_code}")
    try:
        print(json.dumps(response.json(), indent=2))
    except Exception:
        print(response.text[:500])
    return response

1. Authentication

Obtain a JWT access token using the dev user credentials.

In [2]:
auth_resp = requests.post(
    f"{BASE_URL}/tokens",
    headers=HEADERS,
    json={
        "data": {
            "type": "tokens",
            "attributes": {"email": EMAIL, "password": PASSWORD},
        }
    },
)
assert auth_resp.status_code == 200, f"Auth failed: {auth_resp.text}"

tokens = auth_resp.json()["data"]["attributes"]
AUTH_HEADERS = {
    **HEADERS,
    "Authorization": f"Bearer {tokens['access']}",
}

print("Authenticated successfully.")
print(f"Access token: {tokens['access'][:50]}...")

# Cleanup: delete any pre-existing image providers to make notebook idempotent
_existing = requests.get(
    f"{BASE_URL}/providers",
    headers=AUTH_HEADERS,
    params={"filter[provider]": "image", "page[size]": 100},
)
_existing_providers = _existing.json().get("data", [])
if _existing_providers:
    print(f"\nCleaning up {len(_existing_providers)} pre-existing image provider(s)...")
    for _p in _existing_providers:
        requests.delete(f"{BASE_URL}/providers/{_p['id']}", headers=AUTH_HEADERS)
    time.sleep(2)  # Wait for async deletes
    print("Done.")
Authenticated successfully.
Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJhY...

2. Create Image Provider

Create a new image provider with a registry URL as its UID.

In [3]:
provider_resp = pp(
    requests.post(
        f"{BASE_URL}/providers",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "providers",
                "attributes": {
                    "provider": "image",
                    "uid": "ghcr.io",
                    "alias": "GitHub Container Registry",
                },
            }
        },
    )
)

assert provider_resp.status_code == 201, f"Provider creation failed: {provider_resp.text}"
provider_id = provider_resp.json()["data"]["id"]
print(f"\nProvider ID: {provider_id}")
HTTP 201
{
  "data": {
    "type": "providers",
    "id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8",
    "attributes": {
      "alias": "GitHub Container Registry",
      "provider": "image",
      "uid": "ghcr.io"
    }
  },
  "meta": {
    "version": "v1"
  }
}

Provider ID: c7f6770d-83f3-497e-aee2-469dd1cc8cb8

2a. Verify Different Registry URL Formats

Test that various valid registry URLs are accepted.

In [4]:
valid_uids = [
    ("docker.io", "Docker Hub"),
    ("123456789012.dkr.ecr.us-east-1.amazonaws.com", "AWS ECR"),
    ("https://registry.example.com:5000", "Private Registry"),
    ("quay.io", "Quay.io"),
]

created_ids = [provider_id]

for uid, alias in valid_uids:
    resp = requests.post(
        f"{BASE_URL}/providers",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "providers",
                "attributes": {
                    "provider": "image",
                    "uid": uid,
                    "alias": alias,
                },
            }
        },
    )
    status_icon = "\u2705" if resp.status_code == 201 else "\u274c"
    print(f"{status_icon} {uid:55s} -> HTTP {resp.status_code}")
    if resp.status_code == 201:
        created_ids.append(resp.json()["data"]["id"])

print(f"\nTotal created_ids: {len(created_ids)}")
✅ docker.io                                               -> HTTP 201
✅ 123456789012.dkr.ecr.us-east-1.amazonaws.com            -> HTTP 201
✅ https://registry.example.com:5000                       -> HTTP 201
✅ quay.io                                                 -> HTTP 201

Total created_ids: 5

2b. Verify Invalid UIDs Are Rejected

In [5]:
invalid_uids = [
    ("ab", "too short (min_length=3)"),
    ("not valid!", "contains space and exclamation mark"),
]

for uid, reason in invalid_uids:
    resp = requests.post(
        f"{BASE_URL}/providers",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "providers",
                "attributes": {
                    "provider": "image",
                    "uid": uid,
                    "alias": "test",
                },
            }
        },
    )
    status_icon = "\u2705" if resp.status_code == 400 else "\u274c"
    error_code = resp.json()["errors"][0]["code"] if resp.status_code == 400 else "N/A"
    print(f"{status_icon} '{uid}' ({reason}) -> HTTP {resp.status_code}, code={error_code}")
✅ 'ab' (too short (min_length=3)) -> HTTP 400, code=min_length
✅ 'not valid!' (contains space and exclamation mark) -> HTTP 400, code=image-uid

2c. List Providers (filter by image type)

In [6]:
resp = pp(
    requests.get(
        f"{BASE_URL}/providers",
        headers=AUTH_HEADERS,
        params={"filter[provider]": "image"},
    )
)
print(f"\nTotal image providers: {resp.json()['meta']['pagination']['count']}")
HTTP 200
{
  "links": {
    "first": "http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1",
    "last": "http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1",
    "next": null,
    "prev": null
  },
  "data": [
    {
      "type": "providers",
      "id": "47750df7-3ba5-40d9-a47f-d27417604e8f",
      "attributes": {
        "inserted_at": "2026-02-17T16:08:35.005954Z",
        "updated_at": "2026-02-17T16:08:35.005964Z",
        "provider": "image",
        "uid": "quay.io",
        "alias": "Quay.io",
        "connection": {
          "connected": null,
          "last_checked_at": null
        }
      },
      "relationships": {
        "secret": {
          "data": null
        },
        "provider_groups": {
          "meta": {
            "count": 0
          },
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:8080/api/v1/providers/47750df7-3ba5-40d9-a47f-d27417604e8f"
      }
    },
    {
      "type": "providers",
      "id": "2a9ca5fa-7ee2-4125-ac2d-e60060f31e35",
      "attributes": {
        "inserted_at": "2026-02-17T16:08:34.962767Z",
        "updated_at": "2026-02-17T16:08:34.962774Z",
        "provider": "image",
        "uid": "https://registry.example.com:5000",
        "alias": "Private Registry",
        "connection": {
          "connected": null,
          "last_checked_at": null
        }
      },
      "relationships": {
        "secret": {
          "data": null
        },
        "provider_groups": {
          "meta": {
            "count": 0
          },
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:8080/api/v1/providers/2a9ca5fa-7ee2-4125-ac2d-e60060f31e35"
      }
    },
    {
      "type": "providers",
      "id": "f6d28e7a-b336-43ca-ada3-96c3db8fed42",
      "attributes": {
        "inserted_at": "2026-02-17T16:08:34.928376Z",
        "updated_at": "2026-02-17T16:08:34.928384Z",
        "provider": "image",
        "uid": "123456789012.dkr.ecr.us-east-1.amazonaws.com",
        "alias": "AWS ECR",
        "connection": {
          "connected": null,
          "last_checked_at": null
        }
      },
      "relationships": {
        "secret": {
          "data": null
        },
        "provider_groups": {
          "meta": {
            "count": 0
          },
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:8080/api/v1/providers/f6d28e7a-b336-43ca-ada3-96c3db8fed42"
      }
    },
    {
      "type": "providers",
      "id": "741eaabd-eb63-431a-ba2a-4fa06d375453",
      "attributes": {
        "inserted_at": "2026-02-17T16:08:34.891869Z",
        "updated_at": "2026-02-17T16:08:34.891877Z",
        "provider": "image",
        "uid": "docker.io",
        "alias": "Docker Hub",
        "connection": {
          "connected": null,
          "last_checked_at": null
        }
      },
      "relationships": {
        "secret": {
          "data": null
        },
        "provider_groups": {
          "meta": {
            "count": 0
          },
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:8080/api/v1/providers/741eaabd-eb63-431a-ba2a-4fa06d375453"
      }
    },
    {
      "type": "providers",
      "id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8",
      "attributes": {
        "inserted_at": "2026-02-17T16:08:34.844315Z",
        "updated_at": "2026-02-17T16:08:34.844329Z",
        "provider": "image",
        "uid": "ghcr.io",
        "alias": "GitHub Container Registry",
        "connection": {
          "connected": null,
          "last_checked_at": null
        }
      },
      "relationships": {
        "secret": {
          "data": null
        },
        "provider_groups": {
          "meta": {
            "count": 0
          },
          "data": []
        }
      },
      "links": {
        "self": "http://localhost:8080/api/v1/providers/c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
      }
    }
  ],
  "meta": {
    "pagination": {
      "page": 1,
      "pages": 1,
      "count": 5
    },
    "version": "v1"
  }
}

Total image providers: 5

3. Create Provider Secret

The image provider supports multiple auth methods:

  • Docker login: registry_username + registry_password
  • Registry token: registry_token (bearer token)
  • No auth: empty secret {} (for public registries)

Optional scan filters: image_filter, tag_filter, max_images

In [7]:
# 3a. Docker login credentials
secret_resp = pp(
    requests.post(
        f"{BASE_URL}/providers/secrets",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "provider-secrets",
                "attributes": {
                    "name": "GHCR Docker Login",
                    "secret_type": "static",
                    "secret": {
                        "registry_username": "my-github-user",
                        "registry_password": "ghp_example_token_here",
                    },
                },
                "relationships": {
                    "provider": {
                        "data": {"type": "providers", "id": provider_id}
                    }
                },
            }
        },
    )
)

assert secret_resp.status_code == 201, "Secret creation failed"
secret_id = secret_resp.json()["data"]["id"]
print(f"\nSecret ID: {secret_id}")
HTTP 201
{
  "data": {
    "type": "provider-secrets",
    "id": "aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c",
    "attributes": {
      "inserted_at": "2026-02-17T16:08:35.179485Z",
      "updated_at": "2026-02-17T16:08:35.179492Z",
      "name": "GHCR Docker Login",
      "secret_type": "static"
    },
    "relationships": {
      "provider": {
        "data": {
          "type": "providers",
          "id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
        }
      }
    }
  },
  "meta": {
    "version": "v1"
  }
}

Secret ID: aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c
In [8]:
# 3b. Registry token auth (for Docker Hub)
docker_hub_id = created_ids[1]  # docker.io

token_secret_resp = pp(
    requests.post(
        f"{BASE_URL}/providers/secrets",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "provider-secrets",
                "attributes": {
                    "name": "Docker Hub Token",
                    "secret_type": "static",
                    "secret": {
                        "registry_token": "dckr_pat_example_token",
                    },
                },
                "relationships": {
                    "provider": {
                        "data": {"type": "providers", "id": docker_hub_id}
                    }
                },
            }
        },
    )
)

assert token_secret_resp.status_code == 201
HTTP 201
{
  "data": {
    "type": "provider-secrets",
    "id": "c78b85d5-e900-4b72-87ee-368b570726d3",
    "attributes": {
      "inserted_at": "2026-02-17T16:08:35.233380Z",
      "updated_at": "2026-02-17T16:08:35.233387Z",
      "name": "Docker Hub Token",
      "secret_type": "static"
    },
    "relationships": {
      "provider": {
        "data": {
          "type": "providers",
          "id": "741eaabd-eb63-431a-ba2a-4fa06d375453"
        }
      }
    }
  },
  "meta": {
    "version": "v1"
  }
}
In [9]:
# 3c. Secret with scan filters (public registry)
quay_id = created_ids[4]  # quay.io

filter_secret_resp = pp(
    requests.post(
        f"{BASE_URL}/providers/secrets",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "provider-secrets",
                "attributes": {
                    "name": "Quay Public + Filters",
                    "secret_type": "static",
                    "secret": {
                        "image_filter": "prowler.*",
                        "tag_filter": "v[0-9]+\\..*",
                        "max_images": 10,
                    },
                },
                "relationships": {
                    "provider": {
                        "data": {"type": "providers", "id": quay_id}
                    }
                },
            }
        },
    )
)

assert filter_secret_resp.status_code == 201
HTTP 201
{
  "data": {
    "type": "provider-secrets",
    "id": "aa01c5de-a272-4250-8a45-8d53cefbe4e9",
    "attributes": {
      "inserted_at": "2026-02-17T16:08:35.300612Z",
      "updated_at": "2026-02-17T16:08:35.300620Z",
      "name": "Quay Public + Filters",
      "secret_type": "static"
    },
    "relationships": {
      "provider": {
        "data": {
          "type": "providers",
          "id": "47750df7-3ba5-40d9-a47f-d27417604e8f"
        }
      }
    }
  },
  "meta": {
    "version": "v1"
  }
}
In [10]:
# 3d. Completely empty secret (public registry, no filters)
ecr_id = created_ids[2]  # ECR

empty_secret_resp = pp(
    requests.post(
        f"{BASE_URL}/providers/secrets",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "provider-secrets",
                "attributes": {
                    "name": "ECR Public",
                    "secret_type": "static",
                    "secret": {},
                },
                "relationships": {
                    "provider": {
                        "data": {"type": "providers", "id": ecr_id}
                    }
                },
            }
        },
    )
)

assert empty_secret_resp.status_code == 201
HTTP 201
{
  "data": {
    "type": "provider-secrets",
    "id": "537feaa9-54e8-41c5-b984-da523d37a542",
    "attributes": {
      "inserted_at": "2026-02-17T16:08:35.342614Z",
      "updated_at": "2026-02-17T16:08:35.342621Z",
      "name": "ECR Public",
      "secret_type": "static"
    },
    "relationships": {
      "provider": {
        "data": {
          "type": "providers",
          "id": "f6d28e7a-b336-43ca-ada3-96c3db8fed42"
        }
      }
    }
  },
  "meta": {
    "version": "v1"
  }
}

4. Test Provider Connection

The connection test verifies that the registry is accessible with the provided credentials. It uses the SDK's registry adapter to call list_repositories().

This returns a Task (HTTP 202) which we can poll.

In [11]:
conn_resp = pp(
    requests.post(
        f"{BASE_URL}/providers/{provider_id}/connection",
        headers=AUTH_HEADERS,
    )
)

print(f"\nConnection test status: HTTP {conn_resp.status_code}")

if conn_resp.status_code == 202:
    task_id = conn_resp.json()["data"]["id"]
    print(f"Task ID: {task_id}")
    print("Polling task...")

    for _ in range(15):
        time.sleep(2)
        task_resp = requests.get(
            f"{BASE_URL}/tasks/{task_id}", headers=AUTH_HEADERS
        )
        state = task_resp.json()["data"]["attributes"]["state"]
        print(f"  Task state: {state}")
        if state.lower() not in ("executing", "available"):
            break

    # Check the provider's connection status
    provider_detail = requests.get(
        f"{BASE_URL}/providers/{provider_id}", headers=AUTH_HEADERS
    )
    conn = provider_detail.json()["data"]["attributes"].get("connection", {})
    print(f"\nProvider connected: {conn.get('connected')}")
    print(f"Last checked: {conn.get('last_checked_at')}")
HTTP 202
{
  "data": {
    "type": "tasks",
    "id": "847a5748-ad79-419f-a7ec-d0e94c2b522e",
    "attributes": {
      "inserted_at": "2026-02-17T16:08:35.445905Z",
      "completed_at": "2026-02-17T16:08:35.430361Z",
      "name": "provider-connection-check",
      "state": "available",
      "result": null,
      "task_args": {
        "provider_id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
      },
      "metadata": {}
    }
  },
  "meta": {
    "version": "v1"
  }
}

Connection test status: HTTP 202
Task ID: 847a5748-ad79-419f-a7ec-d0e94c2b522e
Polling task...
  Task state: failed

Provider connected: None
Last checked: None

5. Trigger a Scan

Create a new scan for the image provider. The API will dispatch a Celery task that runs the image provider's Trivy-based scanning logic.

In [12]:
scan_resp = pp(
    requests.post(
        f"{BASE_URL}/scans",
        headers=AUTH_HEADERS,
        json={
            "data": {
                "type": "scans",
                "attributes": {
                    "name": "Image Scan - GHCR",
                },
                "relationships": {
                    "provider": {
                        "data": {"type": "providers", "id": provider_id}
                    }
                },
            }
        },
    )
)

print(f"\nScan creation status: HTTP {scan_resp.status_code}")

if scan_resp.status_code in (201, 202):
    scan_data = scan_resp.json()["data"]
    scan_id = scan_data["id"]
    scan_type = scan_data["type"]
    print(f"Response type: {scan_type}")
    print(f"ID: {scan_id}")
    if scan_type == "tasks":
        print(f"Task state: {scan_data['attributes'].get('state')}")
    else:
        print(f"Scan state: {scan_data['attributes'].get('state')}")
HTTP 202
{
  "data": {
    "type": "tasks",
    "id": "4bbc30a6-4ebb-4dc6-ade4-d2d95df84103",
    "attributes": {
      "inserted_at": "2026-02-17T16:20:55.307288Z",
      "completed_at": "2026-02-17T16:20:55.302010Z",
      "name": "scan-perform",
      "state": "available",
      "result": null,
      "task_args": {
        "scan_id": "019c6c67-9b77-7b35-8930-412754c262e3",
        "provider_id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
      },
      "metadata": {}
    }
  },
  "meta": {
    "version": "v1"
  }
}

Scan creation status: HTTP 202
Response type: tasks
ID: 4bbc30a6-4ebb-4dc6-ade4-d2d95df84103
Task state: available

6. Monitor Scan Progress

Poll the scan endpoint until it completes or fails.

In [13]:
# If the scan creation returned a task, find the actual scan ID
if scan_type == "tasks":
    # The task is linked to the scan; get the scan from the scans list
    scans_resp = requests.get(
        f"{BASE_URL}/scans",
        headers=AUTH_HEADERS,
        params={"sort": "-inserted_at", "page[size]": 1},
    )
    scan_id = scans_resp.json()["data"][0]["id"]
    print(f"Found scan ID: {scan_id}")

print("Polling scan progress...")
for i in range(30):
    time.sleep(5)
    resp = requests.get(f"{BASE_URL}/scans/{scan_id}", headers=AUTH_HEADERS)
    data = resp.json()["data"]

    if data["type"] == "tasks":
        state = data["attributes"]["state"]
        print(f"  [{i*5:3d}s] Task state: {state}")
        if state.lower() != "executing":
            break
    else:
        attrs = data["attributes"]
        state = attrs["state"]
        progress = attrs.get("progress", 0)
        resources = attrs.get("unique_resource_count", 0)
        print(f"  [{i*5:3d}s] state={state} progress={progress}% resources={resources}")
        if state.lower() in ("completed", "failed", "cancelled"):
            break

# Final scan state
final = requests.get(f"{BASE_URL}/scans/{scan_id}", headers=AUTH_HEADERS)
print("\nFinal scan state:")
pp(final);
Found scan ID: 019c6c67-9b77-7b35-8930-412754c262e3
Polling scan progress...
  [  0s] state=failed progress=0% resources=0

Final scan state:
HTTP 200
{
  "data": {
    "type": "scans",
    "id": "019c6c67-9b77-7b35-8930-412754c262e3",
    "attributes": {
      "name": "Image Scan - GHCR",
      "trigger": "manual",
      "state": "failed",
      "unique_resource_count": 0,
      "progress": 0,
      "duration": 0,
      "inserted_at": "2026-02-17T16:20:55.287959Z",
      "started_at": "2026-02-17T16:20:55.394134Z",
      "completed_at": "2026-02-17T16:20:55.540383Z",
      "scheduled_at": null,
      "next_scan_at": null
    },
    "relationships": {
      "provider": {
        "data": {
          "type": "providers",
          "id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
        }
      },
      "task": {
        "data": {
          "type": "tasks",
          "id": "4bbc30a6-4ebb-4dc6-ade4-d2d95df84103"
        }
      },
      "processor": {
        "data": null
      }
    },
    "links": {
      "self": "http://localhost:8080/api/v1/scans/019c6c67-9b77-7b35-8930-412754c262e3"
    }
  },
  "meta": {
    "version": "v1"
  }
}

7. Retrieve Findings

List findings from the scan, filtered by scan ID.

In [14]:
findings_resp = pp(
    requests.get(
        f"{BASE_URL}/findings",
        headers=AUTH_HEADERS,
        params={
            "filter[scan]": scan_id,
            "page[size]": 5,
            "sort": "-severity",
        },
    )
)

findings_data = findings_resp.json()
total = findings_data.get("meta", {}).get("pagination", {}).get("count", 0)
print(f"\nTotal findings: {total}")

for f in findings_data.get("data", []):
    attrs = f["attributes"]
    print(
        f"  [{attrs.get('severity', 'N/A'):8s}] "
        f"{attrs.get('status', 'N/A'):4s} - "
        f"{attrs.get('check_id', 'N/A')}"
    )
HTTP 200
{
  "links": {
    "first": "http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6c67-9b77-7b35-8930-412754c262e3&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity",
    "last": "http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6c67-9b77-7b35-8930-412754c262e3&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity",
    "next": null,
    "prev": null
  },
  "data": [],
  "meta": {
    "pagination": {
      "page": 1,
      "pages": 1,
      "count": 0
    },
    "version": "v1"
  }
}

Total findings: 0

7b. Retrieve Resources

List resources discovered during the scan.

In [15]:
from datetime import datetime, timedelta

# Resources endpoint requires a date filter
today = datetime.utcnow().strftime("%Y-%m-%d")

resources_resp = pp(
    requests.get(
        f"{BASE_URL}/resources",
        headers=AUTH_HEADERS,
        params={
            "filter[provider]": provider_id,
            "filter[updated_at.gte]": today,
            "page[size]": 10,
        },
    )
)

resources_data = resources_resp.json()
total_resources = resources_data.get("meta", {}).get("pagination", {}).get("count", 0)
print(f"\nTotal resources: {total_resources}")

for r in resources_data.get("data", []):
    attrs = r["attributes"]
    print(f"  {attrs.get('uid', 'N/A'):60s} type={attrs.get('type', 'N/A')}")
HTTP 200
{
  "links": {
    "first": "http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=c7f6770d-83f3-497e-aee2-469dd1cc8cb8&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10",
    "last": "http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=c7f6770d-83f3-497e-aee2-469dd1cc8cb8&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10",
    "next": null,
    "prev": null
  },
  "data": [],
  "meta": {
    "pagination": {
      "page": 1,
      "pages": 1,
      "count": 0
    },
    "version": "v1"
  }
}

Total resources: 0
/var/folders/3z/2_mtblb94xx5br2qrg1c2y440000gn/T/ipykernel_49619/708590820.py:4: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
  today = datetime.utcnow().strftime("%Y-%m-%d")

8. Cleanup

Delete the test providers created during this notebook.

In [16]:
print(f"Cleaning up {len(created_ids)} provider(s)...")
for pid in created_ids:
    resp = requests.delete(f"{BASE_URL}/providers/{pid}", headers=AUTH_HEADERS)
    print(f"  DELETE {pid} -> HTTP {resp.status_code}")

# Verify cleanup
check = requests.get(
    f"{BASE_URL}/providers",
    headers=AUTH_HEADERS,
    params={"filter[provider]": "image"},
)
remaining = check.json()["meta"]["pagination"]["count"]
print(f"\nRemaining image providers: {remaining}")
Cleaning up 5 provider(s)...
  DELETE c7f6770d-83f3-497e-aee2-469dd1cc8cb8 -> HTTP 202
  DELETE 741eaabd-eb63-431a-ba2a-4fa06d375453 -> HTTP 202
  DELETE f6d28e7a-b336-43ca-ada3-96c3db8fed42 -> HTTP 202
  DELETE 2a9ca5fa-7ee2-4125-ac2d-e60060f31e35 -> HTTP 202
  DELETE 47750df7-3ba5-40d9-a47f-d27417604e8f -> HTTP 202

Remaining image providers: 0

Summary

Endpoint Method Purpose Status
/providers POST Create image provider Tested
/providers GET List providers (filter by image) Tested
/providers/{id} GET Get provider details Tested
/providers/{id} DELETE Soft-delete provider Tested
/providers/{id}/connection POST Test registry connection Tested
/providers/secrets POST Create secret (docker login, token, empty, filters) Tested
/scans POST Trigger image scan Tested
/scans/{id} GET Monitor scan progress Tested
/findings GET Retrieve scan findings Tested
/resources GET Retrieve scan resources Tested