LoFP LoFP / globally distributed applications or ci runners that call bedrock with one shared iam user or role. prefer per-region identities, or raise the country threshold for these principals.

Techniques

Sample rules

AWS Bedrock Model Invocation From High Number of Countries

Description

Detects a single AWS principal invoking Amazon Bedrock models from three or more countries and three or more source IPs within the lookback window. LLMjacking operators often distribute stolen AWS credentials across proxies or resell them to multiple buyers, which produces inference traffic for one identity from many unrelated locations.

Detection logic

from logs-aws.cloudtrail-*
| where
    data_stream.dataset == "aws.cloudtrail" and
    event.provider == "bedrock.amazonaws.com" and
    event.action in ("InvokeModel", "InvokeModelWithResponseStream", "Converse", "ConverseStream") and
    event.outcome == "success" and
    aws.cloudtrail.user_identity.arn is not null and
    source.ip is not null and
    source.geo.country_name is not null
| eval
    Esql.identity = coalesce(
        aws.cloudtrail.user_identity.session_context.session_issuer.arn,
        aws.cloudtrail.user_identity.arn
    )
| stats
    Esql.country_count = count_distinct(source.geo.country_name),
    Esql.source_ip_count = count_distinct(source.ip),
    Esql.user_agent_count = count_distinct(user_agent.original),
    Esql.event_count = count(*),
    Esql.event_action_values = values(event.action),
    Esql.source_ip_values = values(source.ip),
    Esql.source_geo_country_name_values = values(source.geo.country_name),
    Esql.source_geo_city_name_values = values(source.geo.city_name),
    Esql.source_as_organization_name_values = values(source.as.organization.name),
    Esql.user_agent_original_values = values(user_agent.original),
    Esql.aws_cloudtrail_user_identity_arn_values = values(aws.cloudtrail.user_identity.arn),
    Esql.aws_cloudtrail_user_identity_type_values = values(aws.cloudtrail.user_identity.type),
    Esql.aws_cloudtrail_user_identity_access_key_id_values = values(aws.cloudtrail.user_identity.access_key_id),
    Esql.cloud_region_values = values(cloud.region),
    Esql.timestamp_first_seen = min(@timestamp),
    Esql.timestamp_last_seen = max(@timestamp)
  by
    Esql.identity,
    cloud.account.id
| where Esql.country_count >= 3 and Esql.source_ip_count >= 3
| eval source.ip = mv_first(Esql.source_ip_values)
| keep
    Esql.identity,
    cloud.account.id,
    source.ip,
    Esql.country_count,
    Esql.source_ip_count,
    Esql.user_agent_count,
    Esql.event_count,
    Esql.event_action_values,
    Esql.source_ip_values,
    Esql.source_geo_country_name_values,
    Esql.source_geo_city_name_values,
    Esql.source_as_organization_name_values,
    Esql.user_agent_original_values,
    Esql.aws_cloudtrail_user_identity_arn_values,
    Esql.aws_cloudtrail_user_identity_type_values,
    Esql.aws_cloudtrail_user_identity_access_key_id_values,
    Esql.cloud_region_values,
    Esql.timestamp_first_seen,
    Esql.timestamp_last_seen