import sys sys.dont_write_bytecode = True import argparse import os sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from keycheck_common import ( append_jsonl, commit_status_transaction, default_input_file, default_proxy_file, ensure_output_files, iter_findings, load_checked_statuses, load_known_keys, load_known_statuses, load_proxies, mask_secret, read_plain_keys, recover_status_transaction, record_cached_keycheck_occurrence, record_validation_result, require_provider_authority, service_output_dir, should_skip_key, write_keycheck_event, ) SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PARENT_DIR = os.path.dirname(SCRIPT_DIR) SERVICE = "aws" OUTPUT_DIR = os.getenv("KEYCHECK_OUTPUT_DIR") or service_output_dir(SERVICE) INPUT_FILE = os.getenv("KEYCHECK_INPUT_FILE") or default_input_file() PROXY_FILE = os.getenv("KEYCHECK_PROXY_FILE") or default_proxy_file() CHECKED_FILE = os.path.join(OUTPUT_DIR, "awsChecked.txt") RESULTS_FILE = os.path.join(OUTPUT_DIR, "awsResults.jsonl") STATUS_FILES = { "VALID": os.path.join(OUTPUT_DIR, "awsAlive.txt"), "BEDROCK": os.path.join(OUTPUT_DIR, "awsBedrock.txt"), "ADMIN": os.path.join(OUTPUT_DIR, "awsAdmin.txt"), "CANARY": os.path.join(OUTPUT_DIR, "awsCanary.txt"), "QUARANTINED": os.path.join(OUTPUT_DIR, "awsQuarantined.txt"), "ACCESS_DENIED": os.path.join(OUTPUT_DIR, "awsAccessDenied.txt"), "DEAD": os.path.join(OUTPUT_DIR, "awsDead.txt"), "NETWORK": os.path.join(OUTPUT_DIR, "awsNetwork.txt"), "UNKNOWN": os.path.join(OUTPUT_DIR, "awsUnknown.txt"), } BEDROCK_REGIONS = ["us-east-1", "us-west-2", "eu-west-1", "eu-north-1", "ap-northeast-1", "ap-southeast-4"] ANTHROPIC_MESSAGES_PROBE = { "anthropic_version": "bedrock-2023-05-31", "messages": [{"role": "user", "content": "ping"}], "max_tokens": -1, } ANTHROPIC_MESSAGES_LIVE_PING = { "anthropic_version": "bedrock-2023-05-31", "messages": [{"role": "user", "content": "ping"}], "max_tokens": 1, } BEDROCK_MODEL_TESTS = { # Current Anthropic Bedrock runtime IDs. The default probe intentionally uses # invalid max_tokens to validate auth/model access without generating tokens. "anthropic.claude-fable-5": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-fable-5": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-fable-5": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-sonnet-5": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-sonnet-5": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-sonnet-5": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-opus-4-8": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-opus-4-8": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-opus-4-8": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-opus-4-7": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-opus-4-7": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-opus-4-7": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-sonnet-4-6": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-sonnet-4-6": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-sonnet-4-6": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-haiku-4-5-20251001-v1:0": ANTHROPIC_MESSAGES_PROBE, "us.anthropic.claude-haiku-4-5-20251001-v1:0": ANTHROPIC_MESSAGES_PROBE, "global.anthropic.claude-haiku-4-5-20251001-v1:0": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-3-5-sonnet-20241022-v2:0": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-3-5-haiku-20241022-v1:0": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-3-haiku-20240307-v1:0": ANTHROPIC_MESSAGES_PROBE, "anthropic.claude-v2": {"prompt": "\n\nHuman:\n\nAssistant:", "max_tokens_to_sample": -1}, "anthropic.claude-instant-v1": {"prompt": "\n\nHuman:\n\nAssistant:", "max_tokens_to_sample": -1}, } def ensure_files(): ensure_output_files([CHECKED_FILE, RESULTS_FILE, *STATUS_FILES.values()]) recover_status_transaction(CHECKED_FILE, STATUS_FILES) def extract_candidates(input_file, plain_files): seen_plain = set() for item in iter_findings(input_file, ["AWS"]): key = item["raw_v2"] or item["raw"] if key and ":" in key: yield key, item["source"], item["finding"] import re regex = re.compile(r"AKIA[0-9A-Z]{16}:[A-Za-z0-9+/]{40}") for item in read_plain_keys(plain_files, regex): key = item["key"] if key not in seen_plain: seen_plain.add(key) yield key, item["source"], {} def is_dead_aws_error(code): return code in {"InvalidClientTokenId", "SignatureDoesNotMatch", "AuthFailure", "UnrecognizedClientException"} def is_canary_text(value): value = str(value or "").lower() return "canarytokens" in value or "canary token" in value or "is_canary" in value def is_canary_finding(finding): if not isinstance(finding, dict): return False extra = finding.get("ExtraData") or {} if isinstance(extra, dict): if str(extra.get("is_canary", "")).lower() == "true": return True if any(is_canary_text(value) for value in extra.values()): return True return is_canary_text(finding.get("Raw")) or is_canary_text(finding.get("RawV2")) def is_canary_arn(arn): return is_canary_text(arn) def aws_client(session, service, proxy=None, region_name=None, timeout=20): kwargs = {} if region_name: kwargs["region_name"] = region_name from botocore.config import Config kwargs["config"] = Config( proxies=proxy or None, connect_timeout=timeout, read_timeout=timeout, retries={"max_attempts": 1}, ) return session.client(service, **kwargs) def bedrock_validation_allows_invoke(exc): text = str(exc or "").lower() if any(item in text for item in ("operation not allowed", "not authorized", "access denied")): return False # The default probe sends deliberately invalid token limits. If Bedrock only # rejects the payload shape after auth, InvokeModel reached the model path. return any(item in text for item in ("max_tokens", "max_tokens_to_sample", "malformed input", "schema")) def client_error_code(exc): try: return exc.response.get("Error", {}).get("Code", "ClientError") except Exception: return "ClientError" def client_error_message(exc): try: return exc.response.get("Error", {}).get("Message", str(exc)) except Exception: return str(exc) def model_arn(region, model_id): # Cross-region inference profile IDs are not foundation-model ARNs. if model_id.startswith(("us.", "eu.", "jp.", "au.", "global.")): return "*" return f"arn:aws:bedrock:{region}::foundation-model/{model_id}" def iam_policy_source_arn(sts_arn, account): arn = str(sts_arn or "") if ":assumed-role/" in arn: role_part = arn.split(":assumed-role/", 1)[1].split("/", 1)[0] return f"arn:aws:iam::{account}:role/{role_part}" return arn if ":iam::" in arn else "" def simulate_bedrock_activation(session, arn, account, region, model_id, proxy=None, timeout=20): import botocore.exceptions source_arn = iam_policy_source_arn(arn, account) if not source_arn: return {"status": "not_available", "message": "unsupported principal arn for IAM simulation"} actions = [ "bedrock:GetFoundationModelAvailability", "bedrock:ListFoundationModelAgreementOffers", "bedrock:GetUseCaseForModelAccess", "bedrock:PutUseCaseForModelAccess", "bedrock:CreateFoundationModelAgreement", "bedrock:GetInferenceProfile", "bedrock:InvokeModel", ] try: iam = aws_client(session, "iam", proxy, timeout=timeout) response = iam.simulate_principal_policy( PolicySourceArn=source_arn, ActionNames=actions, ResourceArns=[model_arn(region, model_id)], ) except botocore.exceptions.ClientError as exc: return { "status": "access_denied" if client_error_code(exc) == "AccessDenied" else "error", "code": client_error_code(exc), "message": client_error_message(exc)[:500], } decisions = {} for item in response.get("EvaluationResults", []): action = str(item.get("EvalActionName") or "") decisions[action] = str(item.get("EvalDecision") or "") activation_actions = ["bedrock:PutUseCaseForModelAccess", "bedrock:CreateFoundationModelAgreement"] can_activate = all(decisions.get(action) == "allowed" for action in activation_actions) return {"status": "ok", "source_arn": source_arn, "can_activate": can_activate, "decisions": decisions} def check_bedrock_management(session, arn, account, proxy=None, timeout=20, regions=None, models=None, max_attempts=12, debug=False): import botocore.exceptions attempts = [] findings = [] tried = 0 for region in (regions or BEDROCK_REGIONS): bedrock = aws_client(session, "bedrock", proxy, region, timeout) use_case = None try: use_case = bedrock.get_use_case_for_model_access() except botocore.exceptions.ClientError as exc: use_case = {"error_code": client_error_code(exc), "message": client_error_message(exc)[:300]} try: profiles = bedrock.list_inference_profiles(typeEquals="SYSTEM_DEFINED", maxResults=20).get("inferenceProfileSummaries", []) except botocore.exceptions.ClientError as exc: profiles = {"error_code": client_error_code(exc), "message": client_error_message(exc)[:300]} for model_id in (models or list(BEDROCK_MODEL_TESTS.keys())): if max_attempts and tried >= max_attempts: return {"enabled": bool(findings), "findings": findings, "message": "; ".join(attempts[:10])} tried += 1 if debug: print(f" BEDROCK MGMT TRY: region={region}, model={model_id}") item = {"region": region, "model": model_id, "use_case": use_case, "profiles": profiles} try: item["foundation_model"] = bedrock.get_foundation_model(modelIdentifier=model_id).get("modelDetails", {}) except botocore.exceptions.ClientError as exc: item["foundation_model_error"] = {"code": client_error_code(exc), "message": client_error_message(exc)[:300]} try: item["availability"] = bedrock.get_foundation_model_availability(modelId=model_id) except botocore.exceptions.ClientError as exc: item["availability_error"] = {"code": client_error_code(exc), "message": client_error_message(exc)[:300]} try: item["agreement_offers"] = bedrock.list_foundation_model_agreement_offers(modelId=model_id, offerType="ALL") except botocore.exceptions.ClientError as exc: item["agreement_offers_error"] = {"code": client_error_code(exc), "message": client_error_message(exc)[:300]} item["iam_simulation"] = simulate_bedrock_activation(session, arn, account, region, model_id, proxy, timeout) availability = item.get("availability") or {} simulation = item.get("iam_simulation") or {} can_activate = bool(simulation.get("can_activate")) authorized = str(availability.get("authorizationStatus") or "").lower() in ("authorized", "available") if can_activate or authorized: findings.append(item) else: code = (item.get("availability_error") or item.get("foundation_model_error") or {}).get("code") or "checked" attempts.append(f"{region}:{model_id}:can_activate={can_activate}:authorization={availability.get('authorizationStatus') or code}") return {"enabled": bool(findings), "findings": findings, "message": "; ".join(attempts[:10])} def check_bedrock(session, proxy=None, timeout=20, debug=False, regions=None, models=None, max_attempts=12, live_invoke=False): import json import botocore.exceptions attempts = [] accepted = [] tried = 0 model_ids = models or list(BEDROCK_MODEL_TESTS.keys()) for region in (regions or BEDROCK_REGIONS): for model_id in model_ids: if max_attempts and tried >= max_attempts: if accepted: first = accepted[0] return { "enabled": True, "region": first.get("region", ""), "model": first.get("model", ""), "available_models": [f"{item['region']}/{item['model']}" for item in accepted], "message": "Bedrock InvokeModel accepted", } return {"enabled": False, "region": "", "model": "", "available_models": [], "message": "; ".join(attempts[:10]) or "Bedrock probe attempt limit reached"} tried += 1 data = BEDROCK_MODEL_TESTS.get(model_id) if data is None: data = ANTHROPIC_MESSAGES_PROBE if live_invoke and data is ANTHROPIC_MESSAGES_PROBE: data = ANTHROPIC_MESSAGES_LIVE_PING client = aws_client(session, "bedrock-runtime", proxy, region, timeout) if debug: print(f" BEDROCK TRY: region={region}, model={model_id}") try: client.invoke_model(body=json.dumps(data), modelId=model_id) if debug: print(" BEDROCK RESULT: invoke_model succeeded") accepted.append({"region": region, "model": model_id, "message": "invoke_model succeeded"}) continue except client.exceptions.ValidationException as exc: message = str(exc) if bedrock_validation_allows_invoke(exc): # ValidationException for the intentional bad payload means auth/model access passed. if debug: print(f" BEDROCK RESULT: validation_exception_after_auth: {message[:200]}") accepted.append({"region": region, "model": model_id, "message": message[:300]}) else: if debug: print(f" BEDROCK RESULT: validation_rejected: {message[:200]}") attempts.append(f"{region}:{model_id}:validation:{message[:120]}") continue except client.exceptions.AccessDeniedException: if debug: print(" BEDROCK RESULT: access_denied") attempts.append(f"{region}:{model_id}:access_denied") continue except client.exceptions.ResourceNotFoundException: if debug: print(" BEDROCK RESULT: model_not_found") attempts.append(f"{region}:{model_id}:not_found") continue except botocore.exceptions.EndpointConnectionError as exc: if debug: print(f" BEDROCK RESULT: network_error: {str(exc)[:120]}") attempts.append(f"{region}:{model_id}:network:{str(exc)[:80]}") continue except botocore.exceptions.ClientError as exc: code = exc.response.get("Error", {}).get("Code", "ClientError") if debug: print(f" BEDROCK RESULT: {code}: {str(exc)[:160]}") attempts.append(f"{region}:{model_id}:{code}") continue if accepted: first = accepted[0] return { "enabled": True, "region": first.get("region", ""), "model": first.get("model", ""), "available_models": [f"{item['region']}/{item['model']}" for item in accepted], "message": "Bedrock InvokeModel accepted", } return {"enabled": False, "region": "", "model": "", "available_models": [], "message": "; ".join(attempts[:10])} def inspect_iam(session, arn, proxy=None, timeout=20): import botocore.exceptions output = {"admin": False, "quarantined": False, "policy_check": "not_checked", "message": ""} if ":user/" not in arn: output["policy_check"] = "not_user_arn" return output username = arn.rsplit("/", 1)[1] try: iam = aws_client(session, "iam", proxy, timeout=timeout) policies = iam.list_attached_user_policies(UserName=username).get("AttachedPolicies", []) except botocore.exceptions.ClientError as exc: code = exc.response.get("Error", {}).get("Code", "") output["policy_check"] = "access_denied" if code == "AccessDenied" else "error" output["message"] = str(exc) return output output["policy_check"] = "ok" for policy in policies: name = policy.get("PolicyName", "") if "AWSCompromisedKeyQuarantine" in name: output["quarantined"] = True if name == "AdministratorAccess": output["admin"] = True return output def check_key( key, probe_bedrock=False, bedrock_debug=False, proxy=None, timeout=20, bedrock_regions=None, bedrock_models=None, bedrock_max_attempts=12, bedrock_live_invoke=False, probe_bedrock_management=False, ): try: import boto3 import botocore.exceptions except ImportError as exc: return {"status": "UNKNOWN", "message": f"boto3/botocore missing: {exc}"} access_key, secret = key.split(":", 1) session = boto3.Session(aws_access_key_id=access_key, aws_secret_access_key=secret) try: identity = aws_client(session, "sts", proxy, timeout=timeout).get_caller_identity() except botocore.exceptions.EndpointConnectionError as exc: return {"status": "NETWORK", "message": str(exc)} except botocore.exceptions.ClientError as exc: code = exc.response.get("Error", {}).get("Code", "") status = "DEAD" if is_dead_aws_error(code) else "ACCESS_DENIED" return {"status": status, "code": code, "message": str(exc)} except Exception as exc: return {"status": "UNKNOWN", "message": str(exc)} arn = identity.get("Arn", "") if is_canary_arn(arn): return { "status": "CANARY", "account": identity.get("Account", ""), "arn": arn, "admin": False, "quarantined": False, "iam_policy_check": "skipped_canary", "bedrock_enabled": False, "bedrock_region": "", "bedrock_model": "", "bedrock_message": "skipped_canary", "bedrock_management_enabled": False, "bedrock_management_message": "skipped_canary", "message": "canary credential detected from STS arn; skipped IAM/Bedrock probes", } iam_info = inspect_iam(session, arn, proxy, timeout) bedrock_info = {"enabled": False, "region": "", "model": "", "message": "not_checked"} bedrock_management_info = {"enabled": False, "findings": [], "message": "not_checked"} if probe_bedrock: bedrock_info = check_bedrock(session, proxy, timeout, bedrock_debug, bedrock_regions, bedrock_models, bedrock_max_attempts, bedrock_live_invoke) if probe_bedrock_management: bedrock_management_info = check_bedrock_management( session, arn, identity.get("Account", ""), proxy, timeout, bedrock_regions, bedrock_models, bedrock_max_attempts, bedrock_debug, ) if iam_info.get("quarantined"): status = "QUARANTINED" elif bedrock_info.get("enabled"): status = "BEDROCK" else: status = "ADMIN" if iam_info.get("admin") else "VALID" message_parts = [ "sts_ok", f"iam_policy_check={iam_info.get('policy_check')}", ] if probe_bedrock: message_parts.append(f"bedrock_enabled={bedrock_info.get('enabled')}") if bedrock_info.get("region"): message_parts.append(f"bedrock_region={bedrock_info.get('region')}") if bedrock_info.get("model"): message_parts.append(f"bedrock_model={bedrock_info.get('model')}") if probe_bedrock_management: findings = bedrock_management_info.get("findings") or [] can_activate = any((item.get("iam_simulation") or {}).get("can_activate") for item in findings) message_parts.append(f"bedrock_mgmt_enabled={bedrock_management_info.get('enabled')}") message_parts.append(f"bedrock_can_activate={can_activate}") if iam_info.get("message") and iam_info.get("policy_check") != "access_denied": message_parts.append(iam_info.get("message")[:300]) return { "status": status, "account": identity.get("Account", ""), "arn": arn, "admin": iam_info.get("admin", False), "quarantined": iam_info.get("quarantined", False), "iam_policy_check": iam_info.get("policy_check"), "bedrock_enabled": bedrock_info.get("enabled"), "bedrock_region": bedrock_info.get("region"), "bedrock_model": bedrock_info.get("model"), "bedrock_available_models": bedrock_info.get("available_models") or [], "bedrock_message": bedrock_info.get("message"), "bedrock_management_enabled": bedrock_management_info.get("enabled"), "bedrock_management_findings": bedrock_management_info.get("findings") or [], "bedrock_management_message": bedrock_management_info.get("message"), "message": "; ".join(message_parts), } def write_result(key, result, source, finding): write_keycheck_event(SERVICE, RESULTS_FILE, key, result, source, finding, "AWS") message = result.get("message", "") if result.get("status") == "BEDROCK": models = result.get("bedrock_available_models") or [] model_text = ",".join(str(item) for item in models) or f"{result.get('bedrock_region', '')}/{result.get('bedrock_model', '')}".strip("/") message = f"{message}; models={model_text}" commit_status_transaction( CHECKED_FILE, STATUS_FILES, key, result["status"], message, result.get("arn", source), ) record_validation_result(SERVICE, key, result, source, finding, "AWS") def parse_args(): parser = argparse.ArgumentParser(description="AWS key checker") parser.add_argument("--input", default=INPUT_FILE) parser.add_argument("--proxy-file", default=PROXY_FILE) parser.add_argument("--plain", action="append", default=[]) parser.add_argument("--timeout", type=int, default=20) parser.add_argument("--max-keys", type=int, default=0) parser.add_argument("--probe-bedrock", action="store_true") parser.add_argument("--bedrock-debug", action="store_true") parser.add_argument("--bedrock-regions", default=",".join(BEDROCK_REGIONS)) parser.add_argument("--bedrock-models", default=",".join(BEDROCK_MODEL_TESTS)) parser.add_argument("--bedrock-max-attempts", type=int, default=12) parser.add_argument("--bedrock-live-invoke", action="store_true") parser.add_argument("--probe-bedrock-management", action="store_true") parser.add_argument("--retry-network", action="store_true") parser.add_argument("--retry-unknown", action="store_true") parser.add_argument("--retry-valid", action="store_true") parser.add_argument("--recheck-all", action="store_true") return parser.parse_args() def main(): require_provider_authority(SERVICE) args = parse_args() ensure_files() proxy_cycler = load_proxies(args.proxy_file) checked = load_checked_statuses(CHECKED_FILE) known_statuses = load_known_statuses(CHECKED_FILE, STATUS_FILES) known = set(known_statuses) retry_statuses = set() if args.retry_network: retry_statuses.add("NETWORK") if args.retry_unknown: retry_statuses.add("UNKNOWN") if args.retry_valid: retry_statuses.update({"VALID", "BEDROCK", "ADMIN"}) processed = 0 skipped = 0 bedrock_regions = [item.strip() for item in str(args.bedrock_regions or "").split(",") if item.strip()] bedrock_models = [item.strip() for item in str(args.bedrock_models or "").split(",") if item.strip()] for key, source, finding in extract_candidates(args.input, args.plain): if should_skip_key( key, checked, known, args, retry_statuses, service=SERVICE, source=source, finding=finding, detector="AWS", known_statuses=known_statuses, ): skipped += 1 continue if args.max_keys and processed >= args.max_keys: break processed += 1 print(f"\n[{processed}] AWS candidate {mask_secret(key)} from {source}") if is_canary_finding(finding): result = { "status": "CANARY", "message": "canary credential detected in TruffleHog ExtraData; skipped AWS API probes", } else: proxy = next(proxy_cycler) if proxy_cycler else None result = check_key( key, args.probe_bedrock, args.bedrock_debug, proxy, args.timeout, bedrock_regions, bedrock_models, args.bedrock_max_attempts, args.bedrock_live_invoke, args.probe_bedrock_management, ) print(f" STATUS: {result['status']} | {result.get('message', '')[:200]}") if args.probe_bedrock: print( " BEDROCK PING: " f"enabled={result.get('bedrock_enabled')}, " f"region={result.get('bedrock_region') or '-'}, " f"model={result.get('bedrock_model') or '-'}" ) if result.get('bedrock_message'): print(f" BEDROCK RESPONSE: {str(result.get('bedrock_message'))[:300]}") if args.probe_bedrock_management: findings = result.get("bedrock_management_findings") or [] can_activate = any((item.get("iam_simulation") or {}).get("can_activate") for item in findings) print( " BEDROCK MGMT: " f"enabled={result.get('bedrock_management_enabled')}, " f"can_activate={can_activate}, " f"findings={len(findings)}" ) if result.get("bedrock_management_message"): print(f" BEDROCK MGMT RESPONSE: {str(result.get('bedrock_management_message'))[:300]}") write_result(key, result, source, finding) known.add(key) checked[key] = result["status"] print(f"\nDone. Processed={processed}, skipped={skipped}, results={RESULTS_FILE}") if __name__ == "__main__": main()