mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-10-16 08:24:15 +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
42 KiB
42 KiB
In [1]:
import getpass
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... Cleaning up 3 pre-existing image provider(s)... Done.
In [3]:
print("=== DockerHub credentials (docker.io/andoniaf) ===")
dockerhub_username = "andoniaf"
dockerhub_password = getpass.getpass(f"Password/Token for {dockerhub_username}: ")
print("\n=== ECR credentials (714274078102.dkr.ecr.eu-west-1.amazonaws.com) ===")
print("Tip: run `aws ecr get-login-password --region eu-west-1` to get the password")
ecr_username = "AWS"
ecr_password = getpass.getpass(f"ECR password for {ecr_username}: ")=== DockerHub credentials (docker.io/andoniaf) === === ECR credentials (714274078102.dkr.ecr.eu-west-1.amazonaws.com) === Tip: run `aws ecr get-login-password --region eu-west-1` to get the password
In [4]:
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": "1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed",
"attributes": {
"alias": "GitHub Container Registry",
"provider": "image",
"uid": "ghcr.io"
}
},
"meta": {
"version": "v1"
}
}
Provider ID: 1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed
In [5]:
valid_uids = [
("714274078102.dkr.ecr.eu-west-1.amazonaws.com", "AWS ECR"),
("docker.io/andoniaf", "Docker Hub"),
]
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)}")✅ 714274078102.dkr.ecr.eu-west-1.amazonaws.com -> HTTP 201 ✅ docker.io/andoniaf -> HTTP 201 Total created_ids: 3
In [6]:
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 [7]:
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": "d5ed4973-0b4e-4722-974f-bb05b99e95e7",
"attributes": {
"inserted_at": "2026-02-17T17:22:19.173707Z",
"updated_at": "2026-02-17T17:22:19.173716Z",
"provider": "image",
"uid": "docker.io/andoniaf",
"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/d5ed4973-0b4e-4722-974f-bb05b99e95e7"
}
},
{
"type": "providers",
"id": "7f93cb59-6e2a-471a-95af-8bfccfad57d6",
"attributes": {
"inserted_at": "2026-02-17T17:22:19.122114Z",
"updated_at": "2026-02-17T17:22:19.122129Z",
"provider": "image",
"uid": "714274078102.dkr.ecr.eu-west-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/7f93cb59-6e2a-471a-95af-8bfccfad57d6"
}
},
{
"type": "providers",
"id": "1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed",
"attributes": {
"inserted_at": "2026-02-17T17:22:15.656726Z",
"updated_at": "2026-02-17T17:22:15.656764Z",
"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/1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed"
}
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 3
},
"version": "v1"
}
}
Total image providers: 3
In [8]:
# 3a. DockerHub - Docker login credentials
docker_hub_id = created_ids[2] # docker.io/andoniaf
secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "DockerHub Login",
"secret_type": "static",
"secret": {
"registry_username": dockerhub_username,
"registry_password": dockerhub_password,
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": docker_hub_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": "7f7aaecc-d5e3-49b2-ae5d-d230262bcf74",
"attributes": {
"inserted_at": "2026-02-17T17:22:33.354535Z",
"updated_at": "2026-02-17T17:22:33.354683Z",
"name": "DockerHub Login",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "d5ed4973-0b4e-4722-974f-bb05b99e95e7"
}
}
}
},
"meta": {
"version": "v1"
}
}
Secret ID: 7f7aaecc-d5e3-49b2-ae5d-d230262bcf74
In [9]:
# 3b. ECR - Docker login credentials
ecr_id = created_ids[1] # 714274078102.dkr.ecr.eu-west-1.amazonaws.com
token_secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "ECR Docker Login",
"secret_type": "static",
"secret": {
"registry_username": ecr_username,
"registry_password": ecr_password,
},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": ecr_id}
}
},
}
},
)
)
assert token_secret_resp.status_code == 201HTTP 201
{
"data": {
"type": "provider-secrets",
"id": "bfb7c7b6-618a-427b-82e3-ff052974d636",
"attributes": {
"inserted_at": "2026-02-17T17:22:38.317768Z",
"updated_at": "2026-02-17T17:22:38.317852Z",
"name": "ECR Docker Login",
"secret_type": "static"
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "7f93cb59-6e2a-471a-95af-8bfccfad57d6"
}
}
}
},
"meta": {
"version": "v1"
}
}
In [ ]:
# 3c. GHCR - Empty secret (public registry, no auth)
filter_secret_resp = pp(
requests.post(
f"{BASE_URL}/providers/secrets",
headers=AUTH_HEADERS,
json={
"data": {
"type": "provider-secrets",
"attributes": {
"name": "GHCR Public",
"secret_type": "static",
"secret": {},
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": provider_id}
}
},
}
},
)
)
assert filter_secret_resp.status_code == 201In [10]:
conn_resp = pp(
requests.post(
f"{BASE_URL}/providers/{docker_hub_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/{docker_hub_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": "437becf4-82ca-4cb7-9215-57acb6f962be",
"attributes": {
"inserted_at": "2026-02-17T17:22:54.685371Z",
"completed_at": "2026-02-17T17:22:54.672124Z",
"name": "provider-connection-check",
"state": "available",
"result": null,
"task_args": {
"provider_id": "d5ed4973-0b4e-4722-974f-bb05b99e95e7"
},
"metadata": {}
}
},
"meta": {
"version": "v1"
}
}
Connection test status: HTTP 202
Task ID: 437becf4-82ca-4cb7-9215-57acb6f962be
Polling task...
Task state: failed
Provider connected: None
Last checked: None
In [11]:
scan_resp = pp(
requests.post(
f"{BASE_URL}/scans",
headers=AUTH_HEADERS,
json={
"data": {
"type": "scans",
"attributes": {
"name": "Image Scan - DockerHub",
},
"relationships": {
"provider": {
"data": {"type": "providers", "id": docker_hub_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": "340089d4-7207-4d4d-9ffb-c047d224abc3",
"attributes": {
"inserted_at": "2026-02-17T17:23:06.027956Z",
"completed_at": "2026-02-17T17:23:05.998723Z",
"name": "scan-perform",
"state": "available",
"result": null,
"task_args": {
"scan_id": "019c6ca0-8872-7d41-8bc2-edc64dca908f",
"provider_id": "d5ed4973-0b4e-4722-974f-bb05b99e95e7"
},
"metadata": {}
}
},
"meta": {
"version": "v1"
}
}
Scan creation status: HTTP 202
Response type: tasks
ID: 340089d4-7207-4d4d-9ffb-c047d224abc3
Task state: available
In [12]:
# 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: 019c6ca0-8872-7d41-8bc2-edc64dca908f
Polling scan progress...
[ 0s] state=failed progress=0% resources=0
Final scan state:
HTTP 200
{
"data": {
"type": "scans",
"id": "019c6ca0-8872-7d41-8bc2-edc64dca908f",
"attributes": {
"name": "Image Scan - DockerHub",
"trigger": "manual",
"state": "failed",
"unique_resource_count": 0,
"progress": 0,
"duration": 0,
"inserted_at": "2026-02-17T17:23:05.971882Z",
"started_at": "2026-02-17T17:23:06.127765Z",
"completed_at": "2026-02-17T17:23:06.279132Z",
"scheduled_at": null,
"next_scan_at": null
},
"relationships": {
"provider": {
"data": {
"type": "providers",
"id": "d5ed4973-0b4e-4722-974f-bb05b99e95e7"
}
},
"task": {
"data": {
"type": "tasks",
"id": "340089d4-7207-4d4d-9ffb-c047d224abc3"
}
},
"processor": {
"data": null
}
},
"links": {
"self": "http://localhost:8080/api/v1/scans/019c6ca0-8872-7d41-8bc2-edc64dca908f"
}
},
"meta": {
"version": "v1"
}
}
In [13]:
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=019c6ca0-8872-7d41-8bc2-edc64dca908f&page%5Bnumber%5D=1&page%5Bsize%5D=5&sort=-severity",
"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",
"next": null,
"prev": null
},
"data": [],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 0
},
"version": "v1"
}
}
Total findings: 0
In [14]:
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=1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed&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=1a00fe5f-f137-4a63-81f1-8ac7ce0a57ed&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_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).
today = datetime.utcnow().strftime("%Y-%m-%d")
In [13]:
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 2 provider(s)... DELETE 4b62c499-380c-494f-be54-251088a2e404 -> HTTP 404 DELETE da5038fe-b3ff-4d1b-9be1-15fc4df50f00 -> HTTP 404 Remaining image providers: 0