mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-22 11:24:17 +00:00
- Add image provider handling in Scan class alongside IaC (skip check loading, run Trivy directly) - Add image provider API models, serializers, and connection check logic - Point API SDK dependency to this feature branch - Add test notebook for image provider API workflow
52 KiB
52 KiB
In [1]:
import json
import time
import requests
BASE_URL = "http://localhost:8080/api/v1"
CONTENT_TYPE = "application/vnd.api+json"
HEADERS = {"Content-Type": CONTENT_TYPE}
# Dev user credentials
EMAIL = "dev@prowler.com"
PASSWORD = "Thisisapassword123@"
def pp(response):
"""Pretty-print a response with status code."""
print(f"HTTP {response.status_code}")
try:
print(json.dumps(response.json(), indent=2))
except Exception:
print(response.text[:500])
return responseIn [2]:
auth_resp = requests.post(
f"{BASE_URL}/tokens",
headers=HEADERS,
json={
"data": {
"type": "tokens",
"attributes": {"email": EMAIL, "password": PASSWORD},
}
},
)
assert auth_resp.status_code == 200, f"Auth failed: {auth_resp.text}"
tokens = auth_resp.json()["data"]["attributes"]
AUTH_HEADERS = {
**HEADERS,
"Authorization": f"Bearer {tokens['access']}",
}
print("Authenticated successfully.")
print(f"Access token: {tokens['access'][:50]}...")
# Cleanup: delete any pre-existing image providers to make notebook idempotent
_existing = requests.get(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
params={"filter[provider]": "image", "page[size]": 100},
)
_existing_providers = _existing.json().get("data", [])
if _existing_providers:
print(f"\nCleaning up {len(_existing_providers)} pre-existing image provider(s)...")
for _p in _existing_providers:
requests.delete(f"{BASE_URL}/providers/{_p['id']}", headers=AUTH_HEADERS)
time.sleep(2) # Wait for async deletes
print("Done.")Authenticated successfully. Access token: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0eXAiOiJhY...
In [3]:
provider_resp = pp(
requests.post(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
json={
"data": {
"type": "providers",
"attributes": {
"provider": "image",
"uid": "ghcr.io",
"alias": "GitHub Container Registry",
},
}
},
)
)
assert provider_resp.status_code == 201, f"Provider creation failed: {provider_resp.text}"
provider_id = provider_resp.json()["data"]["id"]
print(f"\nProvider ID: {provider_id}")HTTP 201
{
"data": {
"type": "providers",
"id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8",
"attributes": {
"alias": "GitHub Container Registry",
"provider": "image",
"uid": "ghcr.io"
}
},
"meta": {
"version": "v1"
}
}
Provider ID: c7f6770d-83f3-497e-aee2-469dd1cc8cb8
In [4]:
valid_uids = [
("docker.io", "Docker Hub"),
("123456789012.dkr.ecr.us-east-1.amazonaws.com", "AWS ECR"),
("https://registry.example.com:5000", "Private Registry"),
("quay.io", "Quay.io"),
]
created_ids = [provider_id]
for uid, alias in valid_uids:
resp = requests.post(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
json={
"data": {
"type": "providers",
"attributes": {
"provider": "image",
"uid": uid,
"alias": alias,
},
}
},
)
status_icon = "\u2705" if resp.status_code == 201 else "\u274c"
print(f"{status_icon} {uid:55s} -> HTTP {resp.status_code}")
if resp.status_code == 201:
created_ids.append(resp.json()["data"]["id"])
print(f"\nTotal created_ids: {len(created_ids)}")✅ docker.io -> HTTP 201 ✅ 123456789012.dkr.ecr.us-east-1.amazonaws.com -> HTTP 201
✅ https://registry.example.com:5000 -> HTTP 201 ✅ quay.io -> HTTP 201 Total created_ids: 5
In [5]:
invalid_uids = [
("ab", "too short (min_length=3)"),
("not valid!", "contains space and exclamation mark"),
]
for uid, reason in invalid_uids:
resp = requests.post(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
json={
"data": {
"type": "providers",
"attributes": {
"provider": "image",
"uid": uid,
"alias": "test",
},
}
},
)
status_icon = "\u2705" if resp.status_code == 400 else "\u274c"
error_code = resp.json()["errors"][0]["code"] if resp.status_code == 400 else "N/A"
print(f"{status_icon} '{uid}' ({reason}) -> HTTP {resp.status_code}, code={error_code}")✅ 'ab' (too short (min_length=3)) -> HTTP 400, code=min_length ✅ 'not valid!' (contains space and exclamation mark) -> HTTP 400, code=image-uid
In [6]:
resp = pp(
requests.get(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
params={"filter[provider]": "image"},
)
)
print(f"\nTotal image providers: {resp.json()['meta']['pagination']['count']}")HTTP 200
{
"links": {
"first": "http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1",
"last": "http://localhost:8080/api/v1/providers?filter%5Bprovider%5D=image&page%5Bnumber%5D=1",
"next": null,
"prev": null
},
"data": [
{
"type": "providers",
"id": "47750df7-3ba5-40d9-a47f-d27417604e8f",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.005954Z",
"updated_at": "2026-02-17T16:08:35.005964Z",
"provider": "image",
"uid": "quay.io",
"alias": "Quay.io",
"connection": {
"connected": null,
"last_checked_at": null
}
},
"relationships": {
"secret": {
"data": null
},
"provider_groups": {
"meta": {
"count": 0
},
"data": []
}
},
"links": {
"self": "http://localhost:8080/api/v1/providers/47750df7-3ba5-40d9-a47f-d27417604e8f"
}
},
{
"type": "providers",
"id": "2a9ca5fa-7ee2-4125-ac2d-e60060f31e35",
"attributes": {
"inserted_at": "2026-02-17T16:08:34.962767Z",
"updated_at": "2026-02-17T16:08:34.962774Z",
"provider": "image",
"uid": "https://registry.example.com:5000",
"alias": "Private Registry",
"connection": {
"connected": null,
"last_checked_at": null
}
},
"relationships": {
"secret": {
"data": null
},
"provider_groups": {
"meta": {
"count": 0
},
"data": []
}
},
"links": {
"self": "http://localhost:8080/api/v1/providers/2a9ca5fa-7ee2-4125-ac2d-e60060f31e35"
}
},
{
"type": "providers",
"id": "f6d28e7a-b336-43ca-ada3-96c3db8fed42",
"attributes": {
"inserted_at": "2026-02-17T16:08:34.928376Z",
"updated_at": "2026-02-17T16:08:34.928384Z",
"provider": "image",
"uid": "123456789012.dkr.ecr.us-east-1.amazonaws.com",
"alias": "AWS ECR",
"connection": {
"connected": null,
"last_checked_at": null
}
},
"relationships": {
"secret": {
"data": null
},
"provider_groups": {
"meta": {
"count": 0
},
"data": []
}
},
"links": {
"self": "http://localhost:8080/api/v1/providers/f6d28e7a-b336-43ca-ada3-96c3db8fed42"
}
},
{
"type": "providers",
"id": "741eaabd-eb63-431a-ba2a-4fa06d375453",
"attributes": {
"inserted_at": "2026-02-17T16:08:34.891869Z",
"updated_at": "2026-02-17T16:08:34.891877Z",
"provider": "image",
"uid": "docker.io",
"alias": "Docker Hub",
"connection": {
"connected": null,
"last_checked_at": null
}
},
"relationships": {
"secret": {
"data": null
},
"provider_groups": {
"meta": {
"count": 0
},
"data": []
}
},
"links": {
"self": "http://localhost:8080/api/v1/providers/741eaabd-eb63-431a-ba2a-4fa06d375453"
}
},
{
"type": "providers",
"id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8",
"attributes": {
"inserted_at": "2026-02-17T16:08:34.844315Z",
"updated_at": "2026-02-17T16:08:34.844329Z",
"provider": "image",
"uid": "ghcr.io",
"alias": "GitHub Container Registry",
"connection": {
"connected": null,
"last_checked_at": null
}
},
"relationships": {
"secret": {
"data": null
},
"provider_groups": {
"meta": {
"count": 0
},
"data": []
}
},
"links": {
"self": "http://localhost:8080/api/v1/providers/c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 5
},
"version": "v1"
}
}
Total image providers: 5
In [7]:
# 3a. Docker login credentials
secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "GHCR Docker Login",
"secret_type": "static",
"secret": {
"registry_username": "my-github-user",
"registry_password": "ghp_example_token_here",
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": provider_id}
}
},
}
},
)
)
assert secret_resp.status_code == 201, "Secret creation failed"
secret_id = secret_resp.json()["data"]["id"]
print(f"\nSecret ID: {secret_id}")HTTP 201
{
"data": {
"type": "provider-secrets",
"id": "aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.179485Z",
"updated_at": "2026-02-17T16:08:35.179492Z",
"name": "GHCR Docker Login",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
}
}
}
},
"meta": {
"version": "v1"
}
}
Secret ID: aa5f20b9-5bbb-48cf-a3ad-3d8920f7b14c
In [8]:
# 3b. Registry token auth (for Docker Hub)
docker_hub_id = created_ids[1] # docker.io
token_secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "Docker Hub Token",
"secret_type": "static",
"secret": {
"registry_token": "dckr_pat_example_token",
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": docker_hub_id}
}
},
}
},
)
)
assert token_secret_resp.status_code == 201HTTP 201
{
"data": {
"type": "provider-secrets",
"id": "c78b85d5-e900-4b72-87ee-368b570726d3",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.233380Z",
"updated_at": "2026-02-17T16:08:35.233387Z",
"name": "Docker Hub Token",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "741eaabd-eb63-431a-ba2a-4fa06d375453"
}
}
}
},
"meta": {
"version": "v1"
}
}
In [9]:
# 3c. Secret with scan filters (public registry)
quay_id = created_ids[4] # quay.io
filter_secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "Quay Public + Filters",
"secret_type": "static",
"secret": {
"image_filter": "prowler.*",
"tag_filter": "v[0-9]+\\..*",
"max_images": 10,
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": quay_id}
}
},
}
},
)
)
assert filter_secret_resp.status_code == 201HTTP 201
{
"data": {
"type": "provider-secrets",
"id": "aa01c5de-a272-4250-8a45-8d53cefbe4e9",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.300612Z",
"updated_at": "2026-02-17T16:08:35.300620Z",
"name": "Quay Public + Filters",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "47750df7-3ba5-40d9-a47f-d27417604e8f"
}
}
}
},
"meta": {
"version": "v1"
}
}
In [10]:
# 3d. Completely empty secret (public registry, no filters)
ecr_id = created_ids[2] # ECR
empty_secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "ECR Public",
"secret_type": "static",
"secret": {},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": ecr_id}
}
},
}
},
)
)
assert empty_secret_resp.status_code == 201HTTP 201
{
"data": {
"type": "provider-secrets",
"id": "537feaa9-54e8-41c5-b984-da523d37a542",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.342614Z",
"updated_at": "2026-02-17T16:08:35.342621Z",
"name": "ECR Public",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "f6d28e7a-b336-43ca-ada3-96c3db8fed42"
}
}
}
},
"meta": {
"version": "v1"
}
}
In [11]:
conn_resp = pp(
requests.post(
f"{BASE_URL}/providers/{provider_id}/connection",
headers=AUTH_HEADERS,
)
)
print(f"\nConnection test status: HTTP {conn_resp.status_code}")
if conn_resp.status_code == 202:
task_id = conn_resp.json()["data"]["id"]
print(f"Task ID: {task_id}")
print("Polling task...")
for _ in range(15):
time.sleep(2)
task_resp = requests.get(
f"{BASE_URL}/tasks/{task_id}", headers=AUTH_HEADERS
)
state = task_resp.json()["data"]["attributes"]["state"]
print(f" Task state: {state}")
if state.lower() not in ("executing", "available"):
break
# Check the provider's connection status
provider_detail = requests.get(
f"{BASE_URL}/providers/{provider_id}", headers=AUTH_HEADERS
)
conn = provider_detail.json()["data"]["attributes"].get("connection", {})
print(f"\nProvider connected: {conn.get('connected')}")
print(f"Last checked: {conn.get('last_checked_at')}")HTTP 202
{
"data": {
"type": "tasks",
"id": "847a5748-ad79-419f-a7ec-d0e94c2b522e",
"attributes": {
"inserted_at": "2026-02-17T16:08:35.445905Z",
"completed_at": "2026-02-17T16:08:35.430361Z",
"name": "provider-connection-check",
"state": "available",
"result": null,
"task_args": {
"provider_id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
},
"metadata": {}
}
},
"meta": {
"version": "v1"
}
}
Connection test status: HTTP 202
Task ID: 847a5748-ad79-419f-a7ec-d0e94c2b522e
Polling task...
Task state: failed Provider connected: None Last checked: None
In [12]:
scan_resp = pp(
requests.post(
f"{BASE_URL}/scans",
headers=AUTH_HEADERS,
json={
"data": {
"type": "scans",
"attributes": {
"name": "Image Scan - GHCR",
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": provider_id}
}
},
}
},
)
)
print(f"\nScan creation status: HTTP {scan_resp.status_code}")
if scan_resp.status_code in (201, 202):
scan_data = scan_resp.json()["data"]
scan_id = scan_data["id"]
scan_type = scan_data["type"]
print(f"Response type: {scan_type}")
print(f"ID: {scan_id}")
if scan_type == "tasks":
print(f"Task state: {scan_data['attributes'].get('state')}")
else:
print(f"Scan state: {scan_data['attributes'].get('state')}")HTTP 202
{
"data": {
"type": "tasks",
"id": "4bbc30a6-4ebb-4dc6-ade4-d2d95df84103",
"attributes": {
"inserted_at": "2026-02-17T16:20:55.307288Z",
"completed_at": "2026-02-17T16:20:55.302010Z",
"name": "scan-perform",
"state": "available",
"result": null,
"task_args": {
"scan_id": "019c6c67-9b77-7b35-8930-412754c262e3",
"provider_id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
},
"metadata": {}
}
},
"meta": {
"version": "v1"
}
}
Scan creation status: HTTP 202
Response type: tasks
ID: 4bbc30a6-4ebb-4dc6-ade4-d2d95df84103
Task state: available
In [13]:
# If the scan creation returned a task, find the actual scan ID
if scan_type == "tasks":
# The task is linked to the scan; get the scan from the scans list
scans_resp = requests.get(
f"{BASE_URL}/scans",
headers=AUTH_HEADERS,
params={"sort": "-inserted_at", "page[size]": 1},
)
scan_id = scans_resp.json()["data"][0]["id"]
print(f"Found scan ID: {scan_id}")
print("Polling scan progress...")
for i in range(30):
time.sleep(5)
resp = requests.get(f"{BASE_URL}/scans/{scan_id}", headers=AUTH_HEADERS)
data = resp.json()["data"]
if data["type"] == "tasks":
state = data["attributes"]["state"]
print(f" [{i*5:3d}s] Task state: {state}")
if state.lower() != "executing":
break
else:
attrs = data["attributes"]
state = attrs["state"]
progress = attrs.get("progress", 0)
resources = attrs.get("unique_resource_count", 0)
print(f" [{i*5:3d}s] state={state} progress={progress}% resources={resources}")
if state.lower() in ("completed", "failed", "cancelled"):
break
# Final scan state
final = requests.get(f"{BASE_URL}/scans/{scan_id}", headers=AUTH_HEADERS)
print("\nFinal scan state:")
pp(final);Found scan ID: 019c6c67-9b77-7b35-8930-412754c262e3 Polling scan progress...
[ 0s] state=failed progress=0% resources=0
Final scan state:
HTTP 200
{
"data": {
"type": "scans",
"id": "019c6c67-9b77-7b35-8930-412754c262e3",
"attributes": {
"name": "Image Scan - GHCR",
"trigger": "manual",
"state": "failed",
"unique_resource_count": 0,
"progress": 0,
"duration": 0,
"inserted_at": "2026-02-17T16:20:55.287959Z",
"started_at": "2026-02-17T16:20:55.394134Z",
"completed_at": "2026-02-17T16:20:55.540383Z",
"scheduled_at": null,
"next_scan_at": null
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "c7f6770d-83f3-497e-aee2-469dd1cc8cb8"
}
},
"task": {
"data": {
"type": "tasks",
"id": "4bbc30a6-4ebb-4dc6-ade4-d2d95df84103"
}
},
"processor": {
"data": null
}
},
"links": {
"self": "http://localhost:8080/api/v1/scans/019c6c67-9b77-7b35-8930-412754c262e3"
}
},
"meta": {
"version": "v1"
}
}
In [14]:
findings_resp = pp(
requests.get(
f"{BASE_URL}/findings",
headers=AUTH_HEADERS,
params={
"filter[scan]": scan_id,
"page[size]": 5,
"sort": "-severity",
},
)
)
findings_data = findings_resp.json()
total = findings_data.get("meta", {}).get("pagination", {}).get("count", 0)
print(f"\nTotal findings: {total}")
for f in findings_data.get("data", []):
attrs = f["attributes"]
print(
f" [{attrs.get('severity', 'N/A'):8s}] "
f"{attrs.get('status', 'N/A'):4s} - "
f"{attrs.get('check_id', 'N/A')}"
)HTTP 200
{
"links": {
"first": "http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6c67-9b77-7b35-8930-412754c262e3&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity",
"last": "http://localhost:8080/api/v1/findings?filter%5Bscan%5D=019c6c67-9b77-7b35-8930-412754c262e3&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity",
"next": null,
"prev": null
},
"data": [],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 0
},
"version": "v1"
}
}
Total findings: 0
In [15]:
from datetime import datetime, timedelta
# Resources endpoint requires a date filter
today = datetime.utcnow().strftime("%Y-%m-%d")
resources_resp = pp(
requests.get(
f"{BASE_URL}/resources",
headers=AUTH_HEADERS,
params={
"filter[provider]": provider_id,
"filter[updated_at.gte]": today,
"page[size]": 10,
},
)
)
resources_data = resources_resp.json()
total_resources = resources_data.get("meta", {}).get("pagination", {}).get("count", 0)
print(f"\nTotal resources: {total_resources}")
for r in resources_data.get("data", []):
attrs = r["attributes"]
print(f" {attrs.get('uid', 'N/A'):60s} type={attrs.get('type', 'N/A')}")HTTP 200
{
"links": {
"first": "http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=c7f6770d-83f3-497e-aee2-469dd1cc8cb8&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10",
"last": "http://localhost:8080/api/v1/resources?filter%5Bprovider%5D=c7f6770d-83f3-497e-aee2-469dd1cc8cb8&filter%5Bupdated_at.gte%5D=2026-02-17&page%5Bnumber%5D=1&page%5Bsize%5D=10",
"next": null,
"prev": null
},
"data": [],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 0
},
"version": "v1"
}
}
Total resources: 0
/var/folders/3z/2_mtblb94xx5br2qrg1c2y440000gn/T/ipykernel_49619/708590820.py:4: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
today = datetime.utcnow().strftime("%Y-%m-%d")
In [16]:
print(f"Cleaning up {len(created_ids)} provider(s)...")
for pid in created_ids:
resp = requests.delete(f"{BASE_URL}/providers/{pid}", headers=AUTH_HEADERS)
print(f" DELETE {pid} -> HTTP {resp.status_code}")
# Verify cleanup
check = requests.get(
f"{BASE_URL}/providers",
headers=AUTH_HEADERS,
params={"filter[provider]": "image"},
)
remaining = check.json()["meta"]["pagination"]["count"]
print(f"\nRemaining image providers: {remaining}")Cleaning up 5 provider(s)...
DELETE c7f6770d-83f3-497e-aee2-469dd1cc8cb8 -> HTTP 202
DELETE 741eaabd-eb63-431a-ba2a-4fa06d375453 -> HTTP 202 DELETE f6d28e7a-b336-43ca-ada3-96c3db8fed42 -> HTTP 202
DELETE 2a9ca5fa-7ee2-4125-ac2d-e60060f31e35 -> HTTP 202
DELETE 47750df7-3ba5-40d9-a47f-d27417604e8f -> HTTP 202 Remaining image providers: 0