From 293a416e0df20de4cff1f96c2731b7cbdbd87945 Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Wed, 18 Feb 2026 16:00:19 +0100 Subject: [PATCH] chore(image): remove test notebooks --- notebooks/image_provider_api_test.ipynb | 1178 ------------- .../image_provider_api_test_output.ipynb | 1465 ----------------- 2 files changed, 2643 deletions(-) delete mode 100644 notebooks/image_provider_api_test.ipynb delete mode 100644 notebooks/image_provider_api_test_output.ipynb diff --git a/notebooks/image_provider_api_test.ipynb b/notebooks/image_provider_api_test.ipynb deleted file mode 100644 index 3d05cca825..0000000000 --- a/notebooks/image_provider_api_test.ipynb +++ /dev/null @@ -1,1178 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "header", - "metadata": {}, - "source": [ - "# Image Provider API Testing\n", - "\n", - "This notebook tests the Image Provider API endpoints added in PROWLER-940.\n", - "\n", - "**Prerequisites:**\n", - "- API running on `localhost:8080` (`make build-and-run-api-dev`)\n", - "- Dev fixtures loaded (automatic on startup)\n", - "\n", - "**Workflow tested:**\n", - "1. Authentication\n", - "2. Create image provider\n", - "3. Create provider secret (multiple auth methods)\n", - "4. Test provider connection\n", - "5. Trigger a scan\n", - "6. Monitor scan progress\n", - "7. Retrieve findings\n", - "8. Cleanup" - ] - }, - { - "cell_type": "markdown", - "id": "setup-header", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "setup", - "metadata": {}, - "outputs": [], - "source": [ - "import getpass\n", - "import json\n", - "import time\n", - "\n", - "import requests\n", - "\n", - "BASE_URL = \"http://localhost:8080/api/v1\"\n", - "CONTENT_TYPE = \"application/vnd.api+json\"\n", - "HEADERS = {\"Content-Type\": CONTENT_TYPE}\n", - "\n", - "# Dev user credentials\n", - "EMAIL = \"dev@prowler.com\"\n", - "PASSWORD = \"Thisisapassword123@\"\n", - "\n", - "\n", - "def pp(response):\n", - " \"\"\"Pretty-print a response with status code.\"\"\"\n", - " print(f\"HTTP {response.status_code}\")\n", - " try:\n", - " print(json.dumps(response.json(), indent=2))\n", - " except Exception:\n", - " print(response.text[:500])\n", - " return response" - ] - }, - { - "cell_type": "markdown", - "id": "auth-header", - "metadata": {}, - "source": [ - "## 1. Authentication\n", - "\n", - "Obtain a JWT access token using the dev user credentials." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "auth", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Authenticated successfully.\n", - "Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJhY...\n", - "\n", - "Cleaning up 3 pre-existing image provider(s)...\n", - "Done.\n" - ] - } - ], - "source": [ - "auth_resp = requests.post(\n", - " f\"{BASE_URL}/tokens\",\n", - " headers=HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"tokens\",\n", - " \"attributes\": {\"email\": EMAIL, \"password\": PASSWORD},\n", - " }\n", - " },\n", - ")\n", - "assert auth_resp.status_code == 200, f\"Auth failed: {auth_resp.text}\"\n", - "\n", - "tokens = auth_resp.json()[\"data\"][\"attributes\"]\n", - "AUTH_HEADERS = {\n", - " **HEADERS,\n", - " \"Authorization\": f\"Bearer {tokens['access']}\",\n", - "}\n", - "\n", - "print(\"Authenticated successfully.\")\n", - "print(f\"Access token: {tokens['access'][:50]}...\")\n", - "\n", - "# Cleanup: delete any pre-existing image providers to make notebook idempotent\n", - "_existing = requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\", \"page[size]\": 100},\n", - ")\n", - "_existing_providers = _existing.json().get(\"data\", [])\n", - "if _existing_providers:\n", - " print(f\"\\nCleaning up {len(_existing_providers)} pre-existing image provider(s)...\")\n", - " for _p in _existing_providers:\n", - " requests.delete(f\"{BASE_URL}/providers/{_p['id']}\", headers=AUTH_HEADERS)\n", - " time.sleep(2) # Wait for async deletes\n", - " print(\"Done.\")" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "mugv80s4hfi", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "=== DockerHub credentials (docker.io/andoniaf) ===\n", - "\n", - "=== ECR credentials (714274078102.dkr.ecr.eu-west-1.amazonaws.com) ===\n", - "Tip: run `aws ecr get-login-password --region eu-west-1` to get the password\n" - ] - } - ], - "source": [ - "print(\"=== DockerHub credentials (docker.io/andoniaf) ===\")\n", - "dockerhub_username = \"andoniaf\"\n", - "dockerhub_password = getpass.getpass(f\"Password/Token for {dockerhub_username}: \")\n", - "\n", - "print(\"\\n=== ECR credentials (714274078102.dkr.ecr.eu-west-1.amazonaws.com) ===\")\n", - "print(\"Tip: run `aws ecr get-login-password --region eu-west-1` to get the password\")\n", - "ecr_username = \"AWS\"\n", - "ecr_password = getpass.getpass(f\"ECR password for {ecr_username}: \")" - ] - }, - { - "cell_type": "markdown", - "id": "provider-header", - "metadata": {}, - "source": [ - "## 2. Create Image Provider\n", - "\n", - "Create a new `image` provider with a registry URL as its UID." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "create-provider", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed\",\n", - " \"attributes\": {\n", - " \"alias\": \"GitHub Container Registry\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\"\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Provider ID: 1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed\n" - ] - } - ], - "source": [ - "provider_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\",\n", - " \"alias\": \"GitHub Container Registry\",\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert provider_resp.status_code == 201, f\"Provider creation failed: {provider_resp.text}\"\n", - "provider_id = provider_resp.json()[\"data\"][\"id\"]\n", - "print(f\"\\nProvider ID: {provider_id}\")" - ] - }, - { - "cell_type": "markdown", - "id": "provider-variants-header", - "metadata": {}, - "source": [ - "### 2a. Verify Different Registry URL Formats\n", - "\n", - "Test that various valid registry URLs are accepted." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "provider-variants", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ 714274078102.dkr.ecr.eu-west-1.amazonaws.com -> HTTP 201\n", - "✅ docker.io/andoniaf -> HTTP 201\n", - "\n", - "Total created_ids: 3\n" - ] - } - ], - "source": [ - "valid_uids = [\n", - "\t(\"714274078102.dkr.ecr.eu-west-1.amazonaws.com\", \"AWS ECR\"),\n", - " (\"docker.io/andoniaf\", \"Docker Hub\"),\n", - "]\n", - "\n", - "created_ids = [provider_id]\n", - "\n", - "for uid, alias in valid_uids:\n", - " resp = requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": uid,\n", - " \"alias\": alias,\n", - " },\n", - " }\n", - " },\n", - " )\n", - " status_icon = \"\\u2705\" if resp.status_code == 201 else \"\\u274c\"\n", - " print(f\"{status_icon} {uid:55s} -> HTTP {resp.status_code}\")\n", - " if resp.status_code == 201:\n", - " created_ids.append(resp.json()[\"data\"][\"id\"])\n", - "\n", - "print(f\"\\nTotal created_ids: {len(created_ids)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "invalid-header", - "metadata": {}, - "source": [ - "### 2b. Verify Invalid UIDs Are Rejected" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "provider-invalid", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ 'ab' (too short (min_length=3)) -> HTTP 400, code=min_length\n", - "✅ 'not valid!' (contains space and exclamation mark) -> HTTP 400, code=image-uid\n" - ] - } - ], - "source": [ - "invalid_uids = [\n", - " (\"ab\", \"too short (min_length=3)\"),\n", - " (\"not valid!\", \"contains space and exclamation mark\"),\n", - "]\n", - "\n", - "for uid, reason in invalid_uids:\n", - " resp = requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": uid,\n", - " \"alias\": \"test\",\n", - " },\n", - " }\n", - " },\n", - " )\n", - " status_icon = \"\\u2705\" if resp.status_code == 400 else \"\\u274c\"\n", - " error_code = resp.json()[\"errors\"][0][\"code\"] if resp.status_code == 400 else \"N/A\"\n", - " print(f\"{status_icon} '{uid}' ({reason}) -> HTTP {resp.status_code}, code={error_code}\")" - ] - }, - { - "cell_type": "markdown", - "id": "list-header", - "metadata": {}, - "source": [ - "### 2c. List Providers (filter by image type)" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "list-providers", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"first\": \"http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1\",\n", - " \"last\": \"http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"d5ed4973-0b4e-4722-974f-bb05b99e95e7\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:19.173707Z\",\n", - " \"updated_at\": \"2026-02-17T17:22:19.173716Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"docker.io/andoniaf\",\n", - " \"alias\": \"Docker Hub\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/d5ed4973-0b4e-4722-974f-bb05b99e95e7\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"7f93cb59-6e2a-471a-95af-8bfccfad57d6\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:19.122114Z\",\n", - " \"updated_at\": \"2026-02-17T17:22:19.122129Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"714274078102.dkr.ecr.eu-west-1.amazonaws.com\",\n", - " \"alias\": \"AWS ECR\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/7f93cb59-6e2a-471a-95af-8bfccfad57d6\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:15.656726Z\",\n", - " \"updated_at\": \"2026-02-17T17:22:15.656764Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\",\n", - " \"alias\": \"GitHub Container Registry\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed\"\n", - " }\n", - " }\n", - " ],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 3\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total image providers: 3\n" - ] - } - ], - "source": [ - "resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\"},\n", - " )\n", - ")\n", - "print(f\"\\nTotal image providers: {resp.json()['meta']['pagination']['count']}\")" - ] - }, - { - "cell_type": "markdown", - "id": "secret-header", - "metadata": {}, - "source": [ - "## 3. Create Provider Secret\n", - "\n", - "The image provider supports multiple auth methods:\n", - "- **Docker login**: `registry_username` + `registry_password`\n", - "- **Registry token**: `registry_token` (bearer token)\n", - "- **No auth**: empty secret `{}` (for public registries)\n", - "\n", - "Optional scan filters: `image_filter`, `tag_filter`, `max_images`" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "secret-docker-login", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"7f7aaecc-d5e3-49b2-ae5d-d230262bcf74\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:33.354535Z\",\n", - " \"updated_at\": \"2026-02-17T17:22:33.354683Z\",\n", - " \"name\": \"DockerHub Login\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"d5ed4973-0b4e-4722-974f-bb05b99e95e7\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Secret ID: 7f7aaecc-d5e3-49b2-ae5d-d230262bcf74\n" - ] - } - ], - "source": [ - "# 3a. DockerHub - Docker login credentials\n", - "docker_hub_id = created_ids[2] # docker.io/andoniaf\n", - "\n", - "secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"DockerHub Login\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {\n", - " \"registry_username\": dockerhub_username,\n", - " \"registry_password\": dockerhub_password,\n", - " },\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": docker_hub_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert secret_resp.status_code == 201, \"Secret creation failed\"\n", - "secret_id = secret_resp.json()[\"data\"][\"id\"]\n", - "print(f\"\\nSecret ID: {secret_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "secret-token", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"bfb7c7b6-618a-427b-82e3-ff052974d636\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:38.317768Z\",\n", - " \"updated_at\": \"2026-02-17T17:22:38.317852Z\",\n", - " \"name\": \"ECR Docker Login\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"7f93cb59-6e2a-471a-95af-8bfccfad57d6\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# 3b. ECR - Docker login credentials\n", - "ecr_id = created_ids[1] # 714274078102.dkr.ecr.eu-west-1.amazonaws.com\n", - "\n", - "token_secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"ECR Docker Login\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {\n", - " \"registry_username\": ecr_username,\n", - " \"registry_password\": ecr_password,\n", - " },\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": ecr_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert token_secret_resp.status_code == 201" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "secret-filters", - "metadata": {}, - "outputs": [], - "source": [ - "# 3c. GHCR - Empty secret (public registry, no auth)\n", - "filter_secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"GHCR Public\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {},\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": provider_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert filter_secret_resp.status_code == 201" - ] - }, - { - "cell_type": "markdown", - "id": "connection-header", - "metadata": {}, - "source": [ - "## 4. Test Provider Connection\n", - "\n", - "The connection test verifies that the registry is accessible with the provided credentials.\n", - "It uses the SDK's registry adapter to call `list_repositories()`.\n", - "\n", - "This returns a Task (HTTP 202) which we can poll." - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "connection-test", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 202\n", - "{\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"437becf4-82ca-4cb7-9215-57acb6f962be\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:22:54.685371Z\",\n", - " \"completed_at\": \"2026-02-17T17:22:54.672124Z\",\n", - " \"name\": \"provider-connection-check\",\n", - " \"state\": \"available\",\n", - " \"result\": null,\n", - " \"task_args\": {\n", - " \"provider_id\": \"d5ed4973-0b4e-4722-974f-bb05b99e95e7\"\n", - " },\n", - " \"metadata\": {}\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Connection test status: HTTP 202\n", - "Task ID: 437becf4-82ca-4cb7-9215-57acb6f962be\n", - "Polling task...\n", - " Task state: failed\n", - "\n", - "Provider connected: None\n", - "Last checked: None\n" - ] - } - ], - "source": [ - "conn_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/{docker_hub_id}/connection\",\n", - " headers=AUTH_HEADERS,\n", - " )\n", - ")\n", - "\n", - "print(f\"\\nConnection test status: HTTP {conn_resp.status_code}\")\n", - "\n", - "if conn_resp.status_code == 202:\n", - " task_id = conn_resp.json()[\"data\"][\"id\"]\n", - " print(f\"Task ID: {task_id}\")\n", - " print(\"Polling task...\")\n", - "\n", - " for _ in range(15):\n", - " time.sleep(2)\n", - " task_resp = requests.get(\n", - " f\"{BASE_URL}/tasks/{task_id}\", headers=AUTH_HEADERS\n", - " )\n", - " state = task_resp.json()[\"data\"][\"attributes\"][\"state\"]\n", - " print(f\" Task state: {state}\")\n", - " if state.lower() not in (\"executing\", \"available\"):\n", - " break\n", - "\n", - " # Check the provider's connection status\n", - " provider_detail = requests.get(\n", - " f\"{BASE_URL}/providers/{docker_hub_id}\", headers=AUTH_HEADERS\n", - " )\n", - " conn = provider_detail.json()[\"data\"][\"attributes\"].get(\"connection\", {})\n", - " print(f\"\\nProvider connected: {conn.get('connected')}\")\n", - " print(f\"Last checked: {conn.get('last_checked_at')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "scan-header", - "metadata": {}, - "source": [ - "## 5. Trigger a Scan\n", - "\n", - "Create a new scan for the image provider. The API will dispatch a Celery task\n", - "that runs the image provider's Trivy-based scanning logic." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "trigger-scan", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 202\n", - "{\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"340089d4-7207-4d4d-9ffb-c047d224abc3\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T17:23:06.027956Z\",\n", - " \"completed_at\": \"2026-02-17T17:23:05.998723Z\",\n", - " \"name\": \"scan-perform\",\n", - " \"state\": \"available\",\n", - " \"result\": null,\n", - " \"task_args\": {\n", - " \"scan_id\": \"019c6ca0-8872-7d41-8bc2-edc64dca908f\",\n", - " \"provider_id\": \"d5ed4973-0b4e-4722-974f-bb05b99e95e7\"\n", - " },\n", - " \"metadata\": {}\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Scan creation status: HTTP 202\n", - "Response type: tasks\n", - "ID: 340089d4-7207-4d4d-9ffb-c047d224abc3\n", - "Task state: available\n" - ] - } - ], - "source": [ - "scan_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/scans\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"scans\",\n", - " \"attributes\": {\n", - " \"name\": \"Image Scan - DockerHub\",\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": docker_hub_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "print(f\"\\nScan creation status: HTTP {scan_resp.status_code}\")\n", - "\n", - "if scan_resp.status_code in (201, 202):\n", - " scan_data = scan_resp.json()[\"data\"]\n", - " scan_id = scan_data[\"id\"]\n", - " scan_type = scan_data[\"type\"]\n", - " print(f\"Response type: {scan_type}\")\n", - " print(f\"ID: {scan_id}\")\n", - " if scan_type == \"tasks\":\n", - " print(f\"Task state: {scan_data['attributes'].get('state')}\")\n", - " else:\n", - " print(f\"Scan state: {scan_data['attributes'].get('state')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "monitor-header", - "metadata": {}, - "source": [ - "## 6. Monitor Scan Progress\n", - "\n", - "Poll the scan endpoint until it completes or fails." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "monitor-scan", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Found scan ID: 019c6ca0-8872-7d41-8bc2-edc64dca908f\n", - "Polling scan progress...\n", - " [ 0s] state=failed progress=0% resources=0\n", - "\n", - "Final scan state:\n", - "HTTP 200\n", - "{\n", - " \"data\": {\n", - " \"type\": \"scans\",\n", - " \"id\": \"019c6ca0-8872-7d41-8bc2-edc64dca908f\",\n", - " \"attributes\": {\n", - " \"name\": \"Image Scan - DockerHub\",\n", - " \"trigger\": \"manual\",\n", - " \"state\": \"failed\",\n", - " \"unique_resource_count\": 0,\n", - " \"progress\": 0,\n", - " \"duration\": 0,\n", - " \"inserted_at\": \"2026-02-17T17:23:05.971882Z\",\n", - " \"started_at\": \"2026-02-17T17:23:06.127765Z\",\n", - " \"completed_at\": \"2026-02-17T17:23:06.279132Z\",\n", - " \"scheduled_at\": null,\n", - " \"next_scan_at\": null\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"d5ed4973-0b4e-4722-974f-bb05b99e95e7\"\n", - " }\n", - " },\n", - " \"task\": {\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"340089d4-7207-4d4d-9ffb-c047d224abc3\"\n", - " }\n", - " },\n", - " \"processor\": {\n", - " \"data\": null\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/scans/019c6ca0-8872-7d41-8bc2-edc64dca908f\"\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# If the scan creation returned a task, find the actual scan ID\n", - "if scan_type == \"tasks\":\n", - " # The task is linked to the scan; get the scan from the scans list\n", - " scans_resp = requests.get(\n", - " f\"{BASE_URL}/scans\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"sort\": \"-inserted_at\", \"page[size]\": 1},\n", - " )\n", - " scan_id = scans_resp.json()[\"data\"][0][\"id\"]\n", - " print(f\"Found scan ID: {scan_id}\")\n", - "\n", - "print(\"Polling scan progress...\")\n", - "for i in range(30):\n", - " time.sleep(5)\n", - " resp = requests.get(f\"{BASE_URL}/scans/{scan_id}\", headers=AUTH_HEADERS)\n", - " data = resp.json()[\"data\"]\n", - "\n", - " if data[\"type\"] == \"tasks\":\n", - " state = data[\"attributes\"][\"state\"]\n", - " print(f\" [{i*5:3d}s] Task state: {state}\")\n", - " if state.lower() != \"executing\":\n", - " break\n", - " else:\n", - " attrs = data[\"attributes\"]\n", - " state = attrs[\"state\"]\n", - " progress = attrs.get(\"progress\", 0)\n", - " resources = attrs.get(\"unique_resource_count\", 0)\n", - " print(f\" [{i*5:3d}s] state={state} progress={progress}% resources={resources}\")\n", - " if state.lower() in (\"completed\", \"failed\", \"cancelled\"):\n", - " break\n", - "\n", - "# Final scan state\n", - "final = requests.get(f\"{BASE_URL}/scans/{scan_id}\", headers=AUTH_HEADERS)\n", - "print(\"\\nFinal scan state:\")\n", - "pp(final);" - ] - }, - { - "cell_type": "markdown", - "id": "findings-header", - "metadata": {}, - "source": [ - "## 7. Retrieve Findings\n", - "\n", - "List findings from the scan, filtered by scan ID." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "findings", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"first\": \"http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6ca0-8872-7d41-8bc2-edc64dca908f&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity\",\n", - " \"last\": \"http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6ca0-8872-7d41-8bc2-edc64dca908f&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 0\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total findings: 0\n" - ] - } - ], - "source": [ - "findings_resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/findings\",\n", - " headers=AUTH_HEADERS,\n", - " params={\n", - " \"filter[scan]\": scan_id,\n", - " \"page[size]\": 5,\n", - " \"sort\": \"-severity\",\n", - " },\n", - " )\n", - ")\n", - "\n", - "findings_data = findings_resp.json()\n", - "total = findings_data.get(\"meta\", {}).get(\"pagination\", {}).get(\"count\", 0)\n", - "print(f\"\\nTotal findings: {total}\")\n", - "\n", - "for f in findings_data.get(\"data\", []):\n", - " attrs = f[\"attributes\"]\n", - " print(\n", - " f\" [{attrs.get('severity', 'N/A'):8s}] \"\n", - " f\"{attrs.get('status', 'N/A'):4s} - \"\n", - " f\"{attrs.get('check_id', 'N/A')}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "resources-header", - "metadata": {}, - "source": [ - "## 7b. Retrieve Resources\n", - "\n", - "List resources discovered during the scan." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "resources", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"first\": \"http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10\",\n", - " \"last\": \"http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 0\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total resources: 0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/var/folders/3z/2_mtblb94xx5br2qrg1c2y440000gn/T/ipykernel_80028/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).\n", - " today = datetime.utcnow().strftime(\"%Y-%m-%d\")\n" - ] - } - ], - "source": [ - "from datetime import datetime, timedelta\n", - "\n", - "# Resources endpoint requires a date filter\n", - "today = datetime.utcnow().strftime(\"%Y-%m-%d\")\n", - "\n", - "resources_resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/resources\",\n", - " headers=AUTH_HEADERS,\n", - " params={\n", - " \"filter[provider]\": provider_id,\n", - " \"filter[updated_at.gte]\": today,\n", - " \"page[size]\": 10,\n", - " },\n", - " )\n", - ")\n", - "\n", - "resources_data = resources_resp.json()\n", - "total_resources = resources_data.get(\"meta\", {}).get(\"pagination\", {}).get(\"count\", 0)\n", - "print(f\"\\nTotal resources: {total_resources}\")\n", - "\n", - "for r in resources_data.get(\"data\", []):\n", - " attrs = r[\"attributes\"]\n", - " print(f\" {attrs.get('uid', 'N/A'):60s} type={attrs.get('type', 'N/A')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "cleanup-header", - "metadata": {}, - "source": [ - "## 8. Cleanup\n", - "\n", - "Delete the test providers created during this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "cleanup", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cleaning up 2 provider(s)...\n", - " DELETE 4b62c499-380c-494f-be54-251088a2e404 -> HTTP 404\n", - " DELETE da5038fe-b3ff-4d1b-9be1-15fc4df50f00 -> HTTP 404\n", - "\n", - "Remaining image providers: 0\n" - ] - } - ], - "source": [ - "print(f\"Cleaning up {len(created_ids)} provider(s)...\")\n", - "for pid in created_ids:\n", - " resp = requests.delete(f\"{BASE_URL}/providers/{pid}\", headers=AUTH_HEADERS)\n", - " print(f\" DELETE {pid} -> HTTP {resp.status_code}\")\n", - "\n", - "# Verify cleanup\n", - "check = requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\"},\n", - ")\n", - "remaining = check.json()[\"meta\"][\"pagination\"][\"count\"]\n", - "print(f\"\\nRemaining image providers: {remaining}\")" - ] - }, - { - "cell_type": "markdown", - "id": "summary-header", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "| Endpoint | Method | Purpose | Status |\n", - "|----------|--------|---------|--------|\n", - "| `/providers` | POST | Create image provider | Tested |\n", - "| `/providers` | GET | List providers (filter by image) | Tested |\n", - "| `/providers/{id}` | GET | Get provider details | Tested |\n", - "| `/providers/{id}` | DELETE | Soft-delete provider | Tested |\n", - "| `/providers/{id}/connection` | POST | Test registry connection | Tested |\n", - "| `/providers/secrets` | POST | Create secret (docker login, token, empty, filters) | Tested |\n", - "| `/scans` | POST | Trigger image scan | Tested |\n", - "| `/scans/{id}` | GET | Monitor scan progress | Tested |\n", - "| `/findings` | GET | Retrieve scan findings | Tested |\n", - "| `/resources` | GET | Retrieve scan resources | Tested |" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "prowler-api-65oNLnry-py3.12", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/notebooks/image_provider_api_test_output.ipynb b/notebooks/image_provider_api_test_output.ipynb deleted file mode 100644 index 31b086bb0e..0000000000 --- a/notebooks/image_provider_api_test_output.ipynb +++ /dev/null @@ -1,1465 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "header", - "metadata": {}, - "source": [ - "# Image Provider API Testing\n", - "\n", - "This notebook tests the Image Provider API endpoints added in PROWLER-940.\n", - "\n", - "**Prerequisites:**\n", - "- API running on `localhost:8080` (`make build-and-run-api-dev`)\n", - "- Dev fixtures loaded (automatic on startup)\n", - "\n", - "**Workflow tested:**\n", - "1. Authentication\n", - "2. Create image provider\n", - "3. Create provider secret (multiple auth methods)\n", - "4. Test provider connection\n", - "5. Trigger a scan\n", - "6. Monitor scan progress\n", - "7. Retrieve findings\n", - "8. Cleanup" - ] - }, - { - "cell_type": "markdown", - "id": "setup-header", - "metadata": {}, - "source": [ - "## Setup" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "setup", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.103299Z", - "iopub.status.busy": "2026-02-17T16:20:52.103201Z", - "iopub.status.idle": "2026-02-17T16:20:52.137636Z", - "shell.execute_reply": "2026-02-17T16:20:52.137075Z" - } - }, - "outputs": [], - "source": [ - "import json\n", - "import time\n", - "\n", - "import requests\n", - "\n", - "BASE_URL = \"http://localhost:8080/api/v1\"\n", - "CONTENT_TYPE = \"application/vnd.api+json\"\n", - "HEADERS = {\"Content-Type\": CONTENT_TYPE}\n", - "\n", - "# Dev user credentials\n", - "EMAIL = \"dev@prowler.com\"\n", - "PASSWORD = \"Thisisapassword123@\"\n", - "\n", - "\n", - "def pp(response):\n", - " \"\"\"Pretty-print a response with status code.\"\"\"\n", - " print(f\"HTTP {response.status_code}\")\n", - " try:\n", - " print(json.dumps(response.json(), indent=2))\n", - " except Exception:\n", - " print(response.text[:500])\n", - " return response" - ] - }, - { - "cell_type": "markdown", - "id": "auth-header", - "metadata": {}, - "source": [ - "## 1. Authentication\n", - "\n", - "Obtain a JWT access token using the dev user credentials." - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "auth", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.139426Z", - "iopub.status.busy": "2026-02-17T16:20:52.139315Z", - "iopub.status.idle": "2026-02-17T16:20:52.497224Z", - "shell.execute_reply": "2026-02-17T16:20:52.496657Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Authenticated successfully.\n", - "Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJhY...\n" - ] - } - ], - "source": [ - "auth_resp = requests.post(\n", - " f\"{BASE_URL}/tokens\",\n", - " headers=HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"tokens\",\n", - " \"attributes\": {\"email\": EMAIL, \"password\": PASSWORD},\n", - " }\n", - " },\n", - ")\n", - "assert auth_resp.status_code == 200, f\"Auth failed: {auth_resp.text}\"\n", - "\n", - "tokens = auth_resp.json()[\"data\"][\"attributes\"]\n", - "AUTH_HEADERS = {\n", - " **HEADERS,\n", - " \"Authorization\": f\"Bearer {tokens['access']}\",\n", - "}\n", - "\n", - "print(\"Authenticated successfully.\")\n", - "print(f\"Access token: {tokens['access'][:50]}...\")\n", - "\n", - "# Cleanup: delete any pre-existing image providers to make notebook idempotent\n", - "_existing = requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\", \"page[size]\": 100},\n", - ")\n", - "_existing_providers = _existing.json().get(\"data\", [])\n", - "if _existing_providers:\n", - " print(f\"\\nCleaning up {len(_existing_providers)} pre-existing image provider(s)...\")\n", - " for _p in _existing_providers:\n", - " requests.delete(f\"{BASE_URL}/providers/{_p['id']}\", headers=AUTH_HEADERS)\n", - " time.sleep(2) # Wait for async deletes\n", - " print(\"Done.\")" - ] - }, - { - "cell_type": "markdown", - "id": "provider-header", - "metadata": {}, - "source": [ - "## 2. Create Image Provider\n", - "\n", - "Create a new `image` provider with a registry URL as its UID." - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "create-provider", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.498934Z", - "iopub.status.busy": "2026-02-17T16:20:52.498810Z", - "iopub.status.idle": "2026-02-17T16:20:52.546320Z", - "shell.execute_reply": "2026-02-17T16:20:52.545685Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\",\n", - " \"attributes\": {\n", - " \"alias\": \"GitHub Container Registry\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\"\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Provider ID: c7f6770d-83f3-497e-aee2-469dd1cc8cb8\n" - ] - } - ], - "source": [ - "provider_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\",\n", - " \"alias\": \"GitHub Container Registry\",\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert provider_resp.status_code == 201, f\"Provider creation failed: {provider_resp.text}\"\n", - "provider_id = provider_resp.json()[\"data\"][\"id\"]\n", - "print(f\"\\nProvider ID: {provider_id}\")" - ] - }, - { - "cell_type": "markdown", - "id": "provider-variants-header", - "metadata": {}, - "source": [ - "### 2a. Verify Different Registry URL Formats\n", - "\n", - "Test that various valid registry URLs are accepted." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "provider-variants", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.547899Z", - "iopub.status.busy": "2026-02-17T16:20:52.547761Z", - "iopub.status.idle": "2026-02-17T16:20:52.707303Z", - "shell.execute_reply": "2026-02-17T16:20:52.706621Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ docker.io -> HTTP 201\n", - "✅ 123456789012.dkr.ecr.us-east-1.amazonaws.com -> HTTP 201\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ https://registry.example.com:5000 -> HTTP 201\n", - "✅ quay.io -> HTTP 201\n", - "\n", - "Total created_ids: 5\n" - ] - } - ], - "source": [ - "valid_uids = [\n", - " (\"docker.io\", \"Docker Hub\"),\n", - " (\"123456789012.dkr.ecr.us-east-1.amazonaws.com\", \"AWS ECR\"),\n", - " (\"https://registry.example.com:5000\", \"Private Registry\"),\n", - " (\"quay.io\", \"Quay.io\"),\n", - "]\n", - "\n", - "created_ids = [provider_id]\n", - "\n", - "for uid, alias in valid_uids:\n", - " resp = requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": uid,\n", - " \"alias\": alias,\n", - " },\n", - " }\n", - " },\n", - " )\n", - " status_icon = \"\\u2705\" if resp.status_code == 201 else \"\\u274c\"\n", - " print(f\"{status_icon} {uid:55s} -> HTTP {resp.status_code}\")\n", - " if resp.status_code == 201:\n", - " created_ids.append(resp.json()[\"data\"][\"id\"])\n", - "\n", - "print(f\"\\nTotal created_ids: {len(created_ids)}\")" - ] - }, - { - "cell_type": "markdown", - "id": "invalid-header", - "metadata": {}, - "source": [ - "### 2b. Verify Invalid UIDs Are Rejected" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "provider-invalid", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.710148Z", - "iopub.status.busy": "2026-02-17T16:20:52.709957Z", - "iopub.status.idle": "2026-02-17T16:20:52.793781Z", - "shell.execute_reply": "2026-02-17T16:20:52.792969Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "✅ 'ab' (too short (min_length=3)) -> HTTP 400, code=min_length\n", - "✅ 'not valid!' (contains space and exclamation mark) -> HTTP 400, code=image-uid\n" - ] - } - ], - "source": [ - "invalid_uids = [\n", - " (\"ab\", \"too short (min_length=3)\"),\n", - " (\"not valid!\", \"contains space and exclamation mark\"),\n", - "]\n", - "\n", - "for uid, reason in invalid_uids:\n", - " resp = requests.post(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"attributes\": {\n", - " \"provider\": \"image\",\n", - " \"uid\": uid,\n", - " \"alias\": \"test\",\n", - " },\n", - " }\n", - " },\n", - " )\n", - " status_icon = \"\\u2705\" if resp.status_code == 400 else \"\\u274c\"\n", - " error_code = resp.json()[\"errors\"][0][\"code\"] if resp.status_code == 400 else \"N/A\"\n", - " print(f\"{status_icon} '{uid}' ({reason}) -> HTTP {resp.status_code}, code={error_code}\")" - ] - }, - { - "cell_type": "markdown", - "id": "list-header", - "metadata": {}, - "source": [ - "### 2c. List Providers (filter by image type)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "list-providers", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.796076Z", - "iopub.status.busy": "2026-02-17T16:20:52.795899Z", - "iopub.status.idle": "2026-02-17T16:20:52.837238Z", - "shell.execute_reply": "2026-02-17T16:20:52.836528Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"first\": \"http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1\",\n", - " \"last\": \"http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"47750df7-3ba5-40d9-a47f-d27417604e8f\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.005954Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:35.005964Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"quay.io\",\n", - " \"alias\": \"Quay.io\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/47750df7-3ba5-40d9-a47f-d27417604e8f\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"2a9ca5fa-7ee2-4125-ac2d-e60060f31e35\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:34.962767Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:34.962774Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"https://registry.example.com:5000\",\n", - " \"alias\": \"Private Registry\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/2a9ca5fa-7ee2-4125-ac2d-e60060f31e35\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"f6d28e7a-b336-43ca-ada3-96c3db8fed42\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:34.928376Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:34.928384Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"123456789012.dkr.ecr.us-east-1.amazonaws.com\",\n", - " \"alias\": \"AWS ECR\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/f6d28e7a-b336-43ca-ada3-96c3db8fed42\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"741eaabd-eb63-431a-ba2a-4fa06d375453\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:34.891869Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:34.891877Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"docker.io\",\n", - " \"alias\": \"Docker Hub\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/741eaabd-eb63-431a-ba2a-4fa06d375453\"\n", - " }\n", - " },\n", - " {\n", - " \"type\": \"providers\",\n", - " \"id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:34.844315Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:34.844329Z\",\n", - " \"provider\": \"image\",\n", - " \"uid\": \"ghcr.io\",\n", - " \"alias\": \"GitHub Container Registry\",\n", - " \"connection\": {\n", - " \"connected\": null,\n", - " \"last_checked_at\": null\n", - " }\n", - " },\n", - " \"relationships\": {\n", - " \"secret\": {\n", - " \"data\": null\n", - " },\n", - " \"provider_groups\": {\n", - " \"meta\": {\n", - " \"count\": 0\n", - " },\n", - " \"data\": []\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/providers/c7f6770d-83f3-497e-aee2-469dd1cc8cb8\"\n", - " }\n", - " }\n", - " ],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 5\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total image providers: 5\n" - ] - } - ], - "source": [ - "resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\"},\n", - " )\n", - ")\n", - "print(f\"\\nTotal image providers: {resp.json()['meta']['pagination']['count']}\")" - ] - }, - { - "cell_type": "markdown", - "id": "secret-header", - "metadata": {}, - "source": [ - "## 3. Create Provider Secret\n", - "\n", - "The image provider supports multiple auth methods:\n", - "- **Docker login**: `registry_username` + `registry_password`\n", - "- **Registry token**: `registry_token` (bearer token)\n", - "- **No auth**: empty secret `{}` (for public registries)\n", - "\n", - "Optional scan filters: `image_filter`, `tag_filter`, `max_images`" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "secret-docker-login", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.839532Z", - "iopub.status.busy": "2026-02-17T16:20:52.839353Z", - "iopub.status.idle": "2026-02-17T16:20:52.883319Z", - "shell.execute_reply": "2026-02-17T16:20:52.882591Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.179485Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:35.179492Z\",\n", - " \"name\": \"GHCR Docker Login\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Secret ID: aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c\n" - ] - } - ], - "source": [ - "# 3a. Docker login credentials\n", - "secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"GHCR Docker Login\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {\n", - " \"registry_username\": \"my-github-user\",\n", - " \"registry_password\": \"ghp_example_token_here\",\n", - " },\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": provider_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert secret_resp.status_code == 201, \"Secret creation failed\"\n", - "secret_id = secret_resp.json()[\"data\"][\"id\"]\n", - "print(f\"\\nSecret ID: {secret_id}\")" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "secret-token", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.885875Z", - "iopub.status.busy": "2026-02-17T16:20:52.885687Z", - "iopub.status.idle": "2026-02-17T16:20:52.941471Z", - "shell.execute_reply": "2026-02-17T16:20:52.940001Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"c78b85d5-e900-4b72-87ee-368b570726d3\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.233380Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:35.233387Z\",\n", - " \"name\": \"Docker Hub Token\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"741eaabd-eb63-431a-ba2a-4fa06d375453\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# 3b. Registry token auth (for Docker Hub)\n", - "docker_hub_id = created_ids[1] # docker.io\n", - "\n", - "token_secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"Docker Hub Token\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {\n", - " \"registry_token\": \"dckr_pat_example_token\",\n", - " },\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": docker_hub_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert token_secret_resp.status_code == 201" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "secret-filters", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:52.942878Z", - "iopub.status.busy": "2026-02-17T16:20:52.942790Z", - "iopub.status.idle": "2026-02-17T16:20:53.005713Z", - "shell.execute_reply": "2026-02-17T16:20:53.004893Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"aa01c5de-a272-4250-8a45-8d53cefbe4e9\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.300612Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:35.300620Z\",\n", - " \"name\": \"Quay Public + Filters\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"47750df7-3ba5-40d9-a47f-d27417604e8f\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# 3c. Secret with scan filters (public registry)\n", - "quay_id = created_ids[4] # quay.io\n", - "\n", - "filter_secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"Quay Public + Filters\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {\n", - " \"image_filter\": \"prowler.*\",\n", - " \"tag_filter\": \"v[0-9]+\\\\..*\",\n", - " \"max_images\": 10,\n", - " },\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": quay_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert filter_secret_resp.status_code == 201" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "secret-empty", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:53.008368Z", - "iopub.status.busy": "2026-02-17T16:20:53.008019Z", - "iopub.status.idle": "2026-02-17T16:20:53.046086Z", - "shell.execute_reply": "2026-02-17T16:20:53.044773Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 201\n", - "{\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"id\": \"537feaa9-54e8-41c5-b984-da523d37a542\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.342614Z\",\n", - " \"updated_at\": \"2026-02-17T16:08:35.342621Z\",\n", - " \"name\": \"ECR Public\",\n", - " \"secret_type\": \"static\"\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"f6d28e7a-b336-43ca-ada3-96c3db8fed42\"\n", - " }\n", - " }\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# 3d. Completely empty secret (public registry, no filters)\n", - "ecr_id = created_ids[2] # ECR\n", - "\n", - "empty_secret_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/secrets\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"provider-secrets\",\n", - " \"attributes\": {\n", - " \"name\": \"ECR Public\",\n", - " \"secret_type\": \"static\",\n", - " \"secret\": {},\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": ecr_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "assert empty_secret_resp.status_code == 201" - ] - }, - { - "cell_type": "markdown", - "id": "connection-header", - "metadata": {}, - "source": [ - "## 4. Test Provider Connection\n", - "\n", - "The connection test verifies that the registry is accessible with the provided credentials.\n", - "It uses the SDK's registry adapter to call `list_repositories()`.\n", - "\n", - "This returns a Task (HTTP 202) which we can poll." - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "connection-test", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:53.049109Z", - "iopub.status.busy": "2026-02-17T16:20:53.048967Z", - "iopub.status.idle": "2026-02-17T16:20:55.252485Z", - "shell.execute_reply": "2026-02-17T16:20:55.251809Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 202\n", - "{\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"847a5748-ad79-419f-a7ec-d0e94c2b522e\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:08:35.445905Z\",\n", - " \"completed_at\": \"2026-02-17T16:08:35.430361Z\",\n", - " \"name\": \"provider-connection-check\",\n", - " \"state\": \"available\",\n", - " \"result\": null,\n", - " \"task_args\": {\n", - " \"provider_id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\"\n", - " },\n", - " \"metadata\": {}\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Connection test status: HTTP 202\n", - "Task ID: 847a5748-ad79-419f-a7ec-d0e94c2b522e\n", - "Polling task...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " Task state: failed\n", - "\n", - "Provider connected: None\n", - "Last checked: None\n" - ] - } - ], - "source": [ - "conn_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/providers/{provider_id}/connection\",\n", - " headers=AUTH_HEADERS,\n", - " )\n", - ")\n", - "\n", - "print(f\"\\nConnection test status: HTTP {conn_resp.status_code}\")\n", - "\n", - "if conn_resp.status_code == 202:\n", - " task_id = conn_resp.json()[\"data\"][\"id\"]\n", - " print(f\"Task ID: {task_id}\")\n", - " print(\"Polling task...\")\n", - "\n", - " for _ in range(15):\n", - " time.sleep(2)\n", - " task_resp = requests.get(\n", - " f\"{BASE_URL}/tasks/{task_id}\", headers=AUTH_HEADERS\n", - " )\n", - " state = task_resp.json()[\"data\"][\"attributes\"][\"state\"]\n", - " print(f\" Task state: {state}\")\n", - " if state.lower() not in (\"executing\", \"available\"):\n", - " break\n", - "\n", - " # Check the provider's connection status\n", - " provider_detail = requests.get(\n", - " f\"{BASE_URL}/providers/{provider_id}\", headers=AUTH_HEADERS\n", - " )\n", - " conn = provider_detail.json()[\"data\"][\"attributes\"].get(\"connection\", {})\n", - " print(f\"\\nProvider connected: {conn.get('connected')}\")\n", - " print(f\"Last checked: {conn.get('last_checked_at')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "scan-header", - "metadata": {}, - "source": [ - "## 5. Trigger a Scan\n", - "\n", - "Create a new scan for the image provider. The API will dispatch a Celery task\n", - "that runs the image provider's Trivy-based scanning logic." - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "trigger-scan", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:55.259698Z", - "iopub.status.busy": "2026-02-17T16:20:55.259557Z", - "iopub.status.idle": "2026-02-17T16:20:55.319240Z", - "shell.execute_reply": "2026-02-17T16:20:55.318546Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 202\n", - "{\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"4bbc30a6-4ebb-4dc6-ade4-d2d95df84103\",\n", - " \"attributes\": {\n", - " \"inserted_at\": \"2026-02-17T16:20:55.307288Z\",\n", - " \"completed_at\": \"2026-02-17T16:20:55.302010Z\",\n", - " \"name\": \"scan-perform\",\n", - " \"state\": \"available\",\n", - " \"result\": null,\n", - " \"task_args\": {\n", - " \"scan_id\": \"019c6c67-9b77-7b35-8930-412754c262e3\",\n", - " \"provider_id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\"\n", - " },\n", - " \"metadata\": {}\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Scan creation status: HTTP 202\n", - "Response type: tasks\n", - "ID: 4bbc30a6-4ebb-4dc6-ade4-d2d95df84103\n", - "Task state: available\n" - ] - } - ], - "source": [ - "scan_resp = pp(\n", - " requests.post(\n", - " f\"{BASE_URL}/scans\",\n", - " headers=AUTH_HEADERS,\n", - " json={\n", - " \"data\": {\n", - " \"type\": \"scans\",\n", - " \"attributes\": {\n", - " \"name\": \"Image Scan - GHCR\",\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\"type\": \"providers\", \"id\": provider_id}\n", - " }\n", - " },\n", - " }\n", - " },\n", - " )\n", - ")\n", - "\n", - "print(f\"\\nScan creation status: HTTP {scan_resp.status_code}\")\n", - "\n", - "if scan_resp.status_code in (201, 202):\n", - " scan_data = scan_resp.json()[\"data\"]\n", - " scan_id = scan_data[\"id\"]\n", - " scan_type = scan_data[\"type\"]\n", - " print(f\"Response type: {scan_type}\")\n", - " print(f\"ID: {scan_id}\")\n", - " if scan_type == \"tasks\":\n", - " print(f\"Task state: {scan_data['attributes'].get('state')}\")\n", - " else:\n", - " print(f\"Scan state: {scan_data['attributes'].get('state')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "monitor-header", - "metadata": {}, - "source": [ - "## 6. Monitor Scan Progress\n", - "\n", - "Poll the scan endpoint until it completes or fails." - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "monitor-scan", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:20:55.320705Z", - "iopub.status.busy": "2026-02-17T16:20:55.320614Z", - "iopub.status.idle": "2026-02-17T16:21:00.603871Z", - "shell.execute_reply": "2026-02-17T16:21:00.602222Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Found scan ID: 019c6c67-9b77-7b35-8930-412754c262e3\n", - "Polling scan progress...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " [ 0s] state=failed progress=0% resources=0\n", - "\n", - "Final scan state:\n", - "HTTP 200\n", - "{\n", - " \"data\": {\n", - " \"type\": \"scans\",\n", - " \"id\": \"019c6c67-9b77-7b35-8930-412754c262e3\",\n", - " \"attributes\": {\n", - " \"name\": \"Image Scan - GHCR\",\n", - " \"trigger\": \"manual\",\n", - " \"state\": \"failed\",\n", - " \"unique_resource_count\": 0,\n", - " \"progress\": 0,\n", - " \"duration\": 0,\n", - " \"inserted_at\": \"2026-02-17T16:20:55.287959Z\",\n", - " \"started_at\": \"2026-02-17T16:20:55.394134Z\",\n", - " \"completed_at\": \"2026-02-17T16:20:55.540383Z\",\n", - " \"scheduled_at\": null,\n", - " \"next_scan_at\": null\n", - " },\n", - " \"relationships\": {\n", - " \"provider\": {\n", - " \"data\": {\n", - " \"type\": \"providers\",\n", - " \"id\": \"c7f6770d-83f3-497e-aee2-469dd1cc8cb8\"\n", - " }\n", - " },\n", - " \"task\": {\n", - " \"data\": {\n", - " \"type\": \"tasks\",\n", - " \"id\": \"4bbc30a6-4ebb-4dc6-ade4-d2d95df84103\"\n", - " }\n", - " },\n", - " \"processor\": {\n", - " \"data\": null\n", - " }\n", - " },\n", - " \"links\": {\n", - " \"self\": \"http://localhost:8080/api/v1/scans/019c6c67-9b77-7b35-8930-412754c262e3\"\n", - " }\n", - " },\n", - " \"meta\": {\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n" - ] - } - ], - "source": [ - "# If the scan creation returned a task, find the actual scan ID\n", - "if scan_type == \"tasks\":\n", - " # The task is linked to the scan; get the scan from the scans list\n", - " scans_resp = requests.get(\n", - " f\"{BASE_URL}/scans\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"sort\": \"-inserted_at\", \"page[size]\": 1},\n", - " )\n", - " scan_id = scans_resp.json()[\"data\"][0][\"id\"]\n", - " print(f\"Found scan ID: {scan_id}\")\n", - "\n", - "print(\"Polling scan progress...\")\n", - "for i in range(30):\n", - " time.sleep(5)\n", - " resp = requests.get(f\"{BASE_URL}/scans/{scan_id}\", headers=AUTH_HEADERS)\n", - " data = resp.json()[\"data\"]\n", - "\n", - " if data[\"type\"] == \"tasks\":\n", - " state = data[\"attributes\"][\"state\"]\n", - " print(f\" [{i*5:3d}s] Task state: {state}\")\n", - " if state.lower() != \"executing\":\n", - " break\n", - " else:\n", - " attrs = data[\"attributes\"]\n", - " state = attrs[\"state\"]\n", - " progress = attrs.get(\"progress\", 0)\n", - " resources = attrs.get(\"unique_resource_count\", 0)\n", - " print(f\" [{i*5:3d}s] state={state} progress={progress}% resources={resources}\")\n", - " if state.lower() in (\"completed\", \"failed\", \"cancelled\"):\n", - " break\n", - "\n", - "# Final scan state\n", - "final = requests.get(f\"{BASE_URL}/scans/{scan_id}\", headers=AUTH_HEADERS)\n", - "print(\"\\nFinal scan state:\")\n", - "pp(final);" - ] - }, - { - "cell_type": "markdown", - "id": "findings-header", - "metadata": {}, - "source": [ - "## 7. Retrieve Findings\n", - "\n", - "List findings from the scan, filtered by scan ID." - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "findings", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:21:00.612786Z", - "iopub.status.busy": "2026-02-17T16:21:00.612618Z", - "iopub.status.idle": "2026-02-17T16:21:00.678835Z", - "shell.execute_reply": "2026-02-17T16:21:00.677892Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"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\",\n", - " \"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\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 0\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total findings: 0\n" - ] - } - ], - "source": [ - "findings_resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/findings\",\n", - " headers=AUTH_HEADERS,\n", - " params={\n", - " \"filter[scan]\": scan_id,\n", - " \"page[size]\": 5,\n", - " \"sort\": \"-severity\",\n", - " },\n", - " )\n", - ")\n", - "\n", - "findings_data = findings_resp.json()\n", - "total = findings_data.get(\"meta\", {}).get(\"pagination\", {}).get(\"count\", 0)\n", - "print(f\"\\nTotal findings: {total}\")\n", - "\n", - "for f in findings_data.get(\"data\", []):\n", - " attrs = f[\"attributes\"]\n", - " print(\n", - " f\" [{attrs.get('severity', 'N/A'):8s}] \"\n", - " f\"{attrs.get('status', 'N/A'):4s} - \"\n", - " f\"{attrs.get('check_id', 'N/A')}\"\n", - " )" - ] - }, - { - "cell_type": "markdown", - "id": "resources-header", - "metadata": {}, - "source": [ - "## 7b. Retrieve Resources\n", - "\n", - "List resources discovered during the scan." - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "resources", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:21:00.680976Z", - "iopub.status.busy": "2026-02-17T16:21:00.680859Z", - "iopub.status.idle": "2026-02-17T16:21:00.780919Z", - "shell.execute_reply": "2026-02-17T16:21:00.779952Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "HTTP 200\n", - "{\n", - " \"links\": {\n", - " \"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\",\n", - " \"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\",\n", - " \"next\": null,\n", - " \"prev\": null\n", - " },\n", - " \"data\": [],\n", - " \"meta\": {\n", - " \"pagination\": {\n", - " \"page\": 1,\n", - " \"pages\": 1,\n", - " \"count\": 0\n", - " },\n", - " \"version\": \"v1\"\n", - " }\n", - "}\n", - "\n", - "Total resources: 0\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/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).\n", - " today = datetime.utcnow().strftime(\"%Y-%m-%d\")\n" - ] - } - ], - "source": [ - "from datetime import datetime, timedelta\n", - "\n", - "# Resources endpoint requires a date filter\n", - "today = datetime.utcnow().strftime(\"%Y-%m-%d\")\n", - "\n", - "resources_resp = pp(\n", - " requests.get(\n", - " f\"{BASE_URL}/resources\",\n", - " headers=AUTH_HEADERS,\n", - " params={\n", - " \"filter[provider]\": provider_id,\n", - " \"filter[updated_at.gte]\": today,\n", - " \"page[size]\": 10,\n", - " },\n", - " )\n", - ")\n", - "\n", - "resources_data = resources_resp.json()\n", - "total_resources = resources_data.get(\"meta\", {}).get(\"pagination\", {}).get(\"count\", 0)\n", - "print(f\"\\nTotal resources: {total_resources}\")\n", - "\n", - "for r in resources_data.get(\"data\", []):\n", - " attrs = r[\"attributes\"]\n", - " print(f\" {attrs.get('uid', 'N/A'):60s} type={attrs.get('type', 'N/A')}\")" - ] - }, - { - "cell_type": "markdown", - "id": "cleanup-header", - "metadata": {}, - "source": [ - "## 8. Cleanup\n", - "\n", - "Delete the test providers created during this notebook." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "cleanup", - "metadata": { - "execution": { - "iopub.execute_input": "2026-02-17T16:21:00.782934Z", - "iopub.status.busy": "2026-02-17T16:21:00.782823Z", - "iopub.status.idle": "2026-02-17T16:21:01.332306Z", - "shell.execute_reply": "2026-02-17T16:21:01.331324Z" - } - }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Cleaning up 5 provider(s)...\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " DELETE c7f6770d-83f3-497e-aee2-469dd1cc8cb8 -> HTTP 202\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " DELETE 741eaabd-eb63-431a-ba2a-4fa06d375453 -> HTTP 202\n", - " DELETE f6d28e7a-b336-43ca-ada3-96c3db8fed42 -> HTTP 202\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " DELETE 2a9ca5fa-7ee2-4125-ac2d-e60060f31e35 -> HTTP 202\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " DELETE 47750df7-3ba5-40d9-a47f-d27417604e8f -> HTTP 202\n", - "\n", - "Remaining image providers: 0\n" - ] - } - ], - "source": [ - "print(f\"Cleaning up {len(created_ids)} provider(s)...\")\n", - "for pid in created_ids:\n", - " resp = requests.delete(f\"{BASE_URL}/providers/{pid}\", headers=AUTH_HEADERS)\n", - " print(f\" DELETE {pid} -> HTTP {resp.status_code}\")\n", - "\n", - "# Verify cleanup\n", - "check = requests.get(\n", - " f\"{BASE_URL}/providers\",\n", - " headers=AUTH_HEADERS,\n", - " params={\"filter[provider]\": \"image\"},\n", - ")\n", - "remaining = check.json()[\"meta\"][\"pagination\"][\"count\"]\n", - "print(f\"\\nRemaining image providers: {remaining}\")" - ] - }, - { - "cell_type": "markdown", - "id": "summary-header", - "metadata": {}, - "source": [ - "## Summary\n", - "\n", - "| Endpoint | Method | Purpose | Status |\n", - "|----------|--------|---------|--------|\n", - "| `/providers` | POST | Create image provider | Tested |\n", - "| `/providers` | GET | List providers (filter by image) | Tested |\n", - "| `/providers/{id}` | GET | Get provider details | Tested |\n", - "| `/providers/{id}` | DELETE | Soft-delete provider | Tested |\n", - "| `/providers/{id}/connection` | POST | Test registry connection | Tested |\n", - "| `/providers/secrets` | POST | Create secret (docker login, token, empty, filters) | Tested |\n", - "| `/scans` | POST | Trigger image scan | Tested |\n", - "| `/scans/{id}` | GET | Monitor scan progress | Tested |\n", - "| `/findings` | GET | Retrieve scan findings | Tested |\n", - "| `/resources` | GET | Retrieve scan resources | Tested |" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.12.11" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -}