diff --git a/local-app/python-tools/gfl-resource-actions/Makefile b/local-app/python-tools/gfl-resource-actions/Makefile index ef7d0f8e..a4ec2186 100644 --- a/local-app/python-tools/gfl-resource-actions/Makefile +++ b/local-app/python-tools/gfl-resource-actions/Makefile @@ -75,7 +75,7 @@ dist: # Run in dry-run mode run-dry-run: - $(PYTHON) -m $(PACKAGE_NAME).cli --stop --dry-run --resource-type ecr --csv-output-dir ./ --csv-export --csv-per-account + $(PYTHON) -m $(PACKAGE_NAME).cli --stop --dry-run --resource-type all --csv-output-dir ./ --csv-export --csv-per-account # Run to stop resources run-stop: diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management/cli.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management/cli.py index eaeffd50..8e624daf 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_resource_management/cli.py +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management/cli.py @@ -146,7 +146,7 @@ def process_command(args: argparse.Namespace) -> None: exclude_resources = [] if args.resource_type and args.resource_type != "all": # Only include the specified resource type - for res_type in ["ec2", "rds", "eks", "emr", "ecr"]: + for res_type in ["ec2", "rds", "eks", "emr", "ecr", "eks_images"]: if res_type != args.resource_type: exclude_resources.append(res_type) diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management/core.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management/core.py index 43f3467a..4068ca77 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_resource_management/core.py +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management/core.py @@ -709,6 +709,7 @@ def _initialize_resource_dict(self) -> Dict[str, List[Dict[str, Any]]]: "emr_clusters": [], "ecr_images": [], "ecr_old_images": [], + "eks_images": [], } def _discover_account_resources( diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management/discovery.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management/discovery.py index b31ebafe..0aa2237c 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_resource_management/discovery.py +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management/discovery.py @@ -18,12 +18,9 @@ logger, paginate_aws_response, ) +from aws_resource_management.managers.eks import EKSManager # Try to import optional managers -try: - from aws_resource_management.managers import EKSManager -except ImportError: - EKSManager = None try: from aws_resource_management.managers import EMRManager except ImportError: @@ -112,6 +109,10 @@ def discover_resources( ) return [] + # Register new resource type + if resource_type == "eks_images": + discovery_method = self._discover_eks_images + # Use thread pool to speed up discovery across regions resources = [] with concurrent.futures.ThreadPoolExecutor( @@ -583,6 +584,36 @@ def _discover_ecr( logger.error(f"Error discovering ECR repositories in {region}: {e}") return [] + def _discover_eks_images( + self, region: str, resource_ids: Optional[List[str]] = None + ) -> List[Dict[str, Any]]: + """ + Discover EKS cluster images in a region. + + Args: + region: AWS region + resource_ids: Not used (for interface compatibility) + + Returns: + List of EKS image inventory dicts + """ + try: + manager = EKSManager(self.account_id, region) + # Use all known accounts for internal/external ECR detection + from aws_resource_management.utils.aws_core_utils import get_account_list + + accounts = set(acc["account_id"] for acc in get_account_list()) + images = manager.discover_cluster_images([region], accounts=accounts) + # Add account info + for img in images: + img["accountId"] = self.account_id + if self.account_name: + img["accountName"] = self.account_name + return images + except Exception as e: + logger.error(f"Error discovering EKS images in {region}: {e}") + return [] + def discover_ecr_images( credentials: Dict[str, str], @@ -730,6 +761,7 @@ def _get_account_resources_impl( "emr_clusters": [], "ecr_images": [], "ecr_old_images": [], + "eks_images": [], # New: EKS image inventory } try: @@ -828,6 +860,19 @@ def _get_account_resources_impl( else: logger.debug("Skipping ECR discovery as per resource type filter") + # Discover EKS images - only if not excluded + if "eks_images" not in exclude_resources: + logger.info( + f"Discovering EKS cluster images for account {account_name} ({account_id}) in {len(regions)} regions" + ) + try: + resources["eks_images"] = discovery.discover_resources("eks_images", regions) + except Exception as e: + logger.error(f"Error discovering EKS cluster images: {e}") + resources["eks_images"] = [] + else: + logger.debug("Skipping EKS image inventory as per resource type filter") + # Add account information to all resources for resource_type, resource_list in resources.items(): for resource in resource_list: diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management/managers/eks.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management/managers/eks.py index 30b5afef..c2fb8ed1 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_resource_management/managers/eks.py +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management/managers/eks.py @@ -2,7 +2,11 @@ EKS resource manager class. """ -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Set +import subprocess +import json +import tempfile +import os from aws_resource_management.managers.base import ResourceManager from aws_resource_management.utils.config_utils import get_config @@ -553,7 +557,7 @@ def discover_clusters(self, regions: List[str]) -> List[Dict[str, Any]]: for region in regions: try: - eks_client = self.get_boto3_client("eks", region) + eks_client = self.create_client("eks", region) if not eks_client: logger.warning(f"Could not create EKS client in {region}") continue @@ -616,3 +620,188 @@ def discover_clusters(self, regions: List[str]) -> List[Dict[str, Any]]: f"Found a total of {len(all_clusters)} EKS clusters across all regions" ) return all_clusters + + def discover_cluster_images( + self, regions: List[str], accounts: Optional[Set[str]] = None + ) -> List[Dict[str, Any]]: + """ + Discover all container images used in EKS clusters across regions. + + Args: + regions: List of AWS regions + accounts: Set of AWS account IDs (for internal/external ECR detection) + + Returns: + List of image metadata dicts + """ + all_images = [] + clusters = self.discover_clusters(regions) + account_id = self.account_id + if accounts is None: + accounts = set([account_id]) + for cluster in clusters: + cluster_name = cluster["name"] + region = cluster["region"] + try: + kubeconfig_path = self._get_kubeconfig(cluster_name, region, account_id) + images = self._get_image_metadata_from_cluster( + kubeconfig_path, account_id, cluster_name, region, accounts + ) + all_images.extend(images) + os.remove(kubeconfig_path) + except Exception as e: + logger.warning(f"Error processing cluster {cluster_name} in {region}: {e}") + logger.info(f"Discovered {len(all_images)} images across all EKS clusters.") + return all_images + + def _get_kubeconfig(self, cluster_name: str, region: str, account_id: str) -> str: + """ + Generate a kubeconfig file for the given EKS cluster using boto3 and awscli. + + Returns path to the kubeconfig file. + """ + eks = self.create_client("eks", region) + cluster_info = eks.describe_cluster(name=cluster_name)["cluster"] + # Use awscli to get token (assumes awscli is configured for this account) + token = self._get_eks_token(cluster_name, region) + kubeconfig = { + "apiVersion": "v1", + "kind": "Config", + "clusters": [{ + "cluster": { + "server": cluster_info["endpoint"], + "certificate-authority-data": cluster_info["certificateAuthority"]["data"] + }, + "name": cluster_name + }], + "contexts": [{ + "context": { + "cluster": cluster_name, + "user": "eks-user" + }, + "name": f"{cluster_name}-context" + }], + "current-context": f"{cluster_name}-context", + "users": [{ + "name": "eks-user", + "user": {"token": token} + }] + } + fd, path = tempfile.mkstemp(prefix=f"kubeconfig-{account_id}-{cluster_name}-", suffix=".yaml") + with os.fdopen(fd, "w") as f: + json.dump(kubeconfig, f) + return path + + def _get_eks_token(self, cluster_name: str, region: str) -> str: + """ + Get EKS authentication token using awscli. + """ + result = subprocess.check_output([ + "aws", "eks", "get-token", + "--cluster-name", cluster_name, + "--region", region + ]) + token_json = json.loads(result) + return token_json["status"]["token"] + + def _get_image_metadata_from_cluster( + self, + kubeconfig_path: str, + account_id: str, + cluster_name: str, + region: str, + accounts: Set[str], + ) -> List[Dict[str, Any]]: + """ + Use kubectl to get pod/container image info from a cluster. + """ + try: + output = subprocess.check_output([ + "kubectl", "--kubeconfig", kubeconfig_path, + "get", "pods", "--all-namespaces", "-o", "json" + ]) + data = json.loads(output) + images = [] + for item in data.get("items", []): + ns = item["metadata"]["namespace"] + pod = item["metadata"]["name"] + for status in item.get("status", {}).get("containerStatuses", []): + image = status.get("image") + image_id = status.get("imageID") + digest = None + if image_id and "@" in image_id: + digest = image_id.split("@", 1)[1] + parsed = self._parse_image_metadata(image, accounts) + images.append({ + "account_id": account_id, + "region": region, + "cluster_name": cluster_name, + "namespace": ns, + "pod": pod, + "image": image, + "digest": digest, + **parsed + }) + return images + except subprocess.CalledProcessError as e: + logger.warning(f"kubectl error: {e}") + return [] + + def _parse_image_metadata(self, image: str, accounts: Set[str]) -> Dict[str, Any]: + """ + Parse image string to extract registry, repo, tag, ECR info, etc. + """ + result = { + "source": "Unknown", + "registry": None, + "repo": None, + "tag": None, + "ecr_account": None, + "ecr_region": None, + "ecr_type": "N/A" + } + if not image: + return result + if image.startswith("public.ecr.aws/"): + try: + parts = image.split("/") + result["registry"] = "public.ecr.aws" + result["repo"] = "/".join(parts[1:-1] + [parts[-1].split(":")[0]]) + result["tag"] = parts[-1].split(":")[1] if ":" in parts[-1] else "latest" + result["source"] = "AWS Public ECR" + result["ecr_type"] = "external" + except Exception: + result["source"] = "AWS Public ECR (parse error)" + return result + if ".dkr.ecr." in image: + try: + parts = image.split(".dkr.ecr.") + ecr_account = parts[0] + ecr_region = parts[1].split(".")[0] + registry_and_path = image.split(".amazonaws.com/")[1] + repo_tag = registry_and_path.split(":") + result["registry"] = image.split("/")[0] + result["repo"] = repo_tag[0] + result["tag"] = repo_tag[1] if len(repo_tag) > 1 else "latest" + result["source"] = "AWS Private ECR" + result["ecr_account"] = ecr_account + result["ecr_region"] = ecr_region + if ecr_account in accounts: + result["ecr_type"] = "internal" + else: + result["ecr_type"] = "external" + except Exception: + result["source"] = "ECR (parse error)" + else: + try: + parts = image.split("/") + result["registry"] = parts[0] if "." in parts[0] else "docker.io" + repo_tag = parts[-1].split(":") + result["repo"] = "/".join(parts[1:-1] + [repo_tag[0]]) + result["tag"] = repo_tag[1] if len(repo_tag) > 1 else "latest" + result["source"] = "Public or Other" + result["ecr_type"] = "external" + except Exception: + result["source"] = "Unknown" + result["ecr_type"] = "external" + return result diff --git a/local-app/python-tools/gfl-resource-actions/aws_resource_management/resource_registry.py b/local-app/python-tools/gfl-resource-actions/aws_resource_management/resource_registry.py index d7fae21a..d7c61dec 100644 --- a/local-app/python-tools/gfl-resource-actions/aws_resource_management/resource_registry.py +++ b/local-app/python-tools/gfl-resource-actions/aws_resource_management/resource_registry.py @@ -297,3 +297,14 @@ def get_registry() -> ResourceRegistry: ResourceRegistry instance """ return registry + +RESOURCE_REGISTRY = [ + { + "value": "eks_images", + "help": ( + "EKS Cluster Images: Inventory all container images running in EKS clusters " + "across accounts/regions. Useful for vulnerability, supply chain, and compliance analysis. " + "Outputs image, registry, tag, ECR info, and cluster/pod context." + ), + }, +]