Created
November 19, 2025 16:03
-
-
Save yannhowe/8971792f6787d7e6941ae4ea6c398c76 to your computer and use it in GitHub Desktop.
Retrieves container image details and software packages (SBOM) from CrowdStrike Falcon Cloud Security APIs in structured JSON format.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/bin/bash | |
| # CrowdStrike Container Image & Package Scanner (JSON Output) | |
| # | |
| # This script retrieves container image details and software packages (SBOM) | |
| # from CrowdStrike Falcon Cloud Security APIs in JSON format. | |
| # | |
| # Author: CrowdStrike | |
| # Version: 1.0 | |
| # License: MIT | |
| set -e | |
| # Configuration | |
| FALCON_CLOUD=${FALCON_CLOUD:-"us-1"} | |
| BASE_URL="https://api.crowdstrike.com" | |
| # Function to show comprehensive help | |
| show_help() { | |
| cat << 'EOF' | |
| ================================================================================ | |
| CrowdStrike Container Image & Package Scanner (JSON Output) | |
| ================================================================================ | |
| DESCRIPTION: | |
| Retrieves container image details and software packages (SBOM) from | |
| CrowdStrike Falcon Cloud Security APIs in structured JSON format. | |
| PREREQUISITES: | |
| 1. CrowdStrike API credentials with 'falcon-container-image:read' scope | |
| 2. jq command-line JSON processor | |
| 3. curl for API requests | |
| SETUP: | |
| export FALCON_CLIENT_ID="your-client-id" | |
| export FALCON_CLIENT_SECRET="your-client-secret" | |
| export FALCON_CLOUD="us-1" # Optional: us-1, us-2, eu-1, us-gov-1 | |
| USAGE: | |
| ./get-image-packages-json.sh [OPTIONS] | |
| OPTIONS: | |
| --repository REPO Filter by repository name | |
| --registry REG Filter by registry | |
| --tag TAG Filter by tag | |
| --digest DIGEST Filter by image digest | |
| --limit NUM Limit images (default: 3, max: 100) | |
| --package-limit NUM Limit packages per image (default: 1000) | |
| --format FORMAT Output format (default: combined) | |
| -h, --help Show this help | |
| OUTPUT FORMATS: | |
| combined - Images with their packages (default) | |
| images-only - Only image metadata | |
| packages-only - Only packages for first image found | |
| EXAMPLES: | |
| # Get 2 images with packages in combined format | |
| ./get-image-packages-json.sh --limit 2 | |
| # Get nginx images with packages | |
| ./get-image-packages-json.sh --repository nginx --limit 1 | |
| # Get only image metadata from Docker Hub | |
| ./get-image-packages-json.sh --registry docker.io --format images-only | |
| # Get packages for specific image | |
| ./get-image-packages-json.sh --digest sha256:abc123... --format packages-only | |
| # Filter by multiple criteria | |
| ./get-image-packages-json.sh --registry docker.io --tag latest --limit 5 | |
| OUTPUT STRUCTURE (combined format): | |
| { | |
| "images": [ | |
| { | |
| "registry": "docker.io", | |
| "repository": "nginx", | |
| "tag": "latest", | |
| "digest": "sha256:...", | |
| "base_os": "Debian GNU", | |
| "config": {"architecture": "amd64"}, | |
| "packages": [ | |
| { | |
| "package_name_version": "nginx-1.21.6-1", | |
| "type": "apt", | |
| "license": "BSD-2-Clause", | |
| "vulnerability_count": 0, | |
| "vulnerabilities": [] | |
| } | |
| ] | |
| } | |
| ], | |
| "total_images": 1, | |
| "generated_at": "2025-11-19T15:54:48Z" | |
| } | |
| FILTERING: | |
| Available filter fields: | |
| - registry: Container registry (docker.io, gcr.io, etc.) | |
| - repository: Repository name (nginx, alpine, etc.) | |
| - tag: Image tag (latest, stable, v1.0, etc.) | |
| - digest: SHA256 digest | |
| - Multiple filters are combined with AND logic | |
| API ENDPOINTS USED: | |
| - POST /oauth2/token (authentication) | |
| - GET /container-security/combined/images/detail/v1 (image details) | |
| - GET /container-security/combined/packages/v1 (package inventory) | |
| TROUBLESHOOTING: | |
| 1. "Failed to get access token": | |
| - Verify FALCON_CLIENT_ID and FALCON_CLIENT_SECRET | |
| - Check API client has 'falcon-container-image:read' scope | |
| 2. "jq: command not found": | |
| - Install jq: apt-get install jq (Ubuntu) or brew install jq (macOS) | |
| 3. Empty results: | |
| - Check filters aren't too restrictive | |
| - Verify container images exist in your environment | |
| 4. Rate limiting: | |
| - Reduce --limit and --package-limit values | |
| - Add delays between requests if running multiple times | |
| SUPPORT: | |
| CrowdStrike Developer Portal: https://falcon.crowdstrike.com/documentation | |
| Container Security APIs: https://falcon.crowdstrike.com/documentation/page/container-security-apis | |
| ================================================================================ | |
| EOF | |
| } | |
| # Function to show usage summary | |
| show_usage() { | |
| cat << 'EOF' | |
| CrowdStrike Container Image & Package Scanner (JSON Output) | |
| Usage: ./get-image-packages-json.sh [OPTIONS] | |
| Common Examples: | |
| ./get-image-packages-json.sh --limit 2 | |
| ./get-image-packages-json.sh --repository nginx --limit 1 | |
| ./get-image-packages-json.sh --registry docker.io --format images-only | |
| Setup Required: | |
| export FALCON_CLIENT_ID="your-client-id" | |
| export FALCON_CLIENT_SECRET="your-client-secret" | |
| Use --help for detailed documentation. | |
| EOF | |
| } | |
| # Check if no arguments provided | |
| if [[ $# -eq 0 ]]; then | |
| show_usage | |
| exit 0 | |
| fi | |
| # Check required environment variables | |
| if [[ -z "$FALCON_CLIENT_ID" || -z "$FALCON_CLIENT_SECRET" ]]; then | |
| echo "❌ ERROR: Missing CrowdStrike API credentials" >&2 | |
| echo "" >&2 | |
| echo "Required environment variables:" >&2 | |
| echo " export FALCON_CLIENT_ID=\"your-client-id\"" >&2 | |
| echo " export FALCON_CLIENT_SECRET=\"your-client-secret\"" >&2 | |
| echo "" >&2 | |
| echo "Optional:" >&2 | |
| echo " export FALCON_CLOUD=\"us-1\" # us-1, us-2, eu-1, us-gov-1" >&2 | |
| echo "" >&2 | |
| echo "Get API credentials from: https://falcon.crowdstrike.com/api-clients-and-keys" >&2 | |
| echo "Required scope: falcon-container-image:read" >&2 | |
| echo "" >&2 | |
| echo "Use --help for complete documentation." >&2 | |
| exit 1 | |
| fi | |
| # Check for jq dependency | |
| if ! command -v jq &> /dev/null; then | |
| echo "❌ ERROR: jq is required but not installed" >&2 | |
| echo "" >&2 | |
| echo "Install jq:" >&2 | |
| echo " Ubuntu/Debian: sudo apt-get install jq" >&2 | |
| echo " macOS: brew install jq" >&2 | |
| echo " RHEL/CentOS: sudo yum install jq" >&2 | |
| exit 1 | |
| fi | |
| # Function to get OAuth2 token | |
| get_token() { | |
| local response | |
| response=$(curl -s -X POST "$BASE_URL/oauth2/token" \ | |
| -H "Content-Type: application/x-www-form-urlencoded" \ | |
| -d "client_id=$FALCON_CLIENT_ID&client_secret=$FALCON_CLIENT_SECRET") | |
| local token | |
| token=$(echo "$response" | jq -r '.access_token') | |
| if [[ "$token" == "null" ]]; then | |
| echo "Failed to get access token:" >&2 | |
| echo "$response" | jq '.' >&2 | |
| exit 1 | |
| fi | |
| echo "$token" | |
| } | |
| # Function to get image details | |
| get_image_details() { | |
| local filter="$1" | |
| local limit="${2:-3}" | |
| local params="limit=$limit&with_config=true" | |
| if [[ -n "$filter" ]]; then | |
| params="$params&filter=$filter" | |
| fi | |
| curl -s -X GET "$BASE_URL/container-security/combined/images/detail/v1?$params" \ | |
| -H "Authorization: Bearer $ACCESS_TOKEN" \ | |
| -H "Content-Type: application/json" | |
| } | |
| # Function to get packages for specific image | |
| get_image_packages() { | |
| local image_digest="$1" | |
| local limit="${2:-1000}" | |
| local filter="image_digest:'$image_digest'" | |
| local params="limit=$limit&filter=$filter" | |
| curl -s -X GET "$BASE_URL/container-security/combined/packages/v1?$params" \ | |
| -H "Authorization: Bearer $ACCESS_TOKEN" \ | |
| -H "Content-Type: application/json" | |
| } | |
| # Parse command line arguments | |
| REPOSITORY="" | |
| REGISTRY="" | |
| TAG="" | |
| IMAGE_DIGEST="" | |
| LIMIT=3 | |
| PACKAGE_LIMIT=1000 | |
| OUTPUT_FORMAT="combined" # combined, images-only, packages-only | |
| while [[ $# -gt 0 ]]; do | |
| case $1 in | |
| --repository) | |
| REPOSITORY="$2" | |
| shift 2 | |
| ;; | |
| --registry) | |
| REGISTRY="$2" | |
| shift 2 | |
| ;; | |
| --tag) | |
| TAG="$2" | |
| shift 2 | |
| ;; | |
| --digest) | |
| IMAGE_DIGEST="$2" | |
| shift 2 | |
| ;; | |
| --limit) | |
| LIMIT="$2" | |
| shift 2 | |
| ;; | |
| --package-limit) | |
| PACKAGE_LIMIT="$2" | |
| shift 2 | |
| ;; | |
| --format) | |
| OUTPUT_FORMAT="$2" | |
| shift 2 | |
| ;; | |
| -h|--help) | |
| show_help | |
| exit 0 | |
| ;; | |
| *) | |
| echo "Unknown option: $1" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| done | |
| # Get access token (suppress output to stderr) | |
| ACCESS_TOKEN=$(get_token) | |
| # Build filter string for images | |
| FILTERS=() | |
| if [[ -n "$REPOSITORY" ]]; then | |
| FILTERS+=("repository:'$REPOSITORY'") | |
| fi | |
| if [[ -n "$REGISTRY" ]]; then | |
| FILTERS+=("registry:'$REGISTRY'") | |
| fi | |
| if [[ -n "$TAG" ]]; then | |
| FILTERS+=("tag:'$TAG'") | |
| fi | |
| if [[ -n "$IMAGE_DIGEST" ]]; then | |
| FILTERS+=("image_digest:'$IMAGE_DIGEST'") | |
| fi | |
| FILTER_STRING="" | |
| if [[ ${#FILTERS[@]} -gt 0 ]]; then | |
| FILTER_STRING=$(IFS='+'; echo "${FILTERS[*]}") | |
| fi | |
| # Get image details | |
| IMAGE_RESPONSE=$(get_image_details "$FILTER_STRING" "$LIMIT") | |
| # Check for errors | |
| if echo "$IMAGE_RESPONSE" | jq -e '.errors | length > 0' > /dev/null 2>&1; then | |
| echo "$IMAGE_RESPONSE" | jq '{error: "API Error", details: .errors}' | |
| exit 1 | |
| fi | |
| # Handle different output formats | |
| case $OUTPUT_FORMAT in | |
| "images-only") | |
| echo "$IMAGE_RESPONSE" | |
| exit 0 | |
| ;; | |
| "packages-only") | |
| # Get first image digest and return only packages | |
| FIRST_DIGEST=$(echo "$IMAGE_RESPONSE" | jq -r '.resources[0].digest // empty') | |
| if [[ -n "$FIRST_DIGEST" ]]; then | |
| get_image_packages "$FIRST_DIGEST" "$PACKAGE_LIMIT" | |
| else | |
| echo '{"error": "No images found or no digest available", "resources": []}' | |
| fi | |
| exit 0 | |
| ;; | |
| "combined") | |
| # Default: combine images with their packages | |
| ;; | |
| *) | |
| echo '{"error": "Invalid format. Use: combined, images-only, or packages-only"}' >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| # Process images and combine with packages | |
| COMBINED_RESULT='{"images": []}' | |
| # Process each image | |
| IMAGES=$(echo "$IMAGE_RESPONSE" | jq -r '.resources[] | @base64') | |
| if [[ -z "$IMAGES" ]]; then | |
| echo '{"images": [], "message": "No images found matching the criteria"}' | |
| exit 0 | |
| fi | |
| for image_data in $IMAGES; do | |
| # Decode image data | |
| IMAGE_JSON=$(echo "$image_data" | base64 --decode) | |
| # Get digest | |
| DIGEST=$(echo "$IMAGE_JSON" | jq -r '.digest // ""') | |
| if [[ -n "$DIGEST" && "$DIGEST" != "null" ]]; then | |
| # Get packages for this image | |
| PACKAGE_RESPONSE=$(get_image_packages "$DIGEST" "$PACKAGE_LIMIT") | |
| # Check for package errors | |
| if echo "$PACKAGE_RESPONSE" | jq -e '.errors | length > 0' > /dev/null 2>&1; then | |
| PACKAGES='{"error": "Failed to get packages", "details": []}' | |
| else | |
| PACKAGES=$(echo "$PACKAGE_RESPONSE" | jq '.resources') | |
| fi | |
| else | |
| PACKAGES='[]' | |
| fi | |
| # Combine image with packages | |
| COMBINED_IMAGE=$(echo "$IMAGE_JSON" | jq --argjson packages "$PACKAGES" '. + {packages: $packages}') | |
| # Add to result | |
| COMBINED_RESULT=$(echo "$COMBINED_RESULT" | jq --argjson image "$COMBINED_IMAGE" '.images += [$image]') | |
| done | |
| # Add metadata | |
| TOTAL_IMAGES=$(echo "$IMAGE_RESPONSE" | jq '.resources | length') | |
| COMBINED_RESULT=$(echo "$COMBINED_RESULT" | jq --argjson total "$TOTAL_IMAGES" '. + {total_images: $total, generated_at: now | strftime("%Y-%m-%dT%H:%M:%SZ")}') | |
| # Output final JSON | |
| echo "$COMBINED_RESULT" | jq '.' |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment