{ "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 }