Back to Checks
aws

No potential LLM jacking activity detected in CloudTrail

cloudtrail_threat_detection_llm_jacking
Service: cloudtrailSeverity: critical
OfficialBuilt-inVerified
Categories:threat-detectiongen-ai
Resource Type:AwsCloudTrailTrail

CloudTrail Bedrock activity is analyzed per identity for a high diversity of LLM-related API calls (e.g., InvokeModel, InvokeModelWithResponseStream, GetFoundationModelAvailability). If an identity's share of these actions exceeds a configured threshold over a recent window, it is surfaced as potential LLM-jacking behavior.

Risk

Such patterns suggest stolen credential abuse to drive LLM usage.

  • Availability: cost exhaustion and service disruption
  • Confidentiality: leakage of prompts/outputs and model settings
  • Integrity: misuse of permissions for broader access Attackers may use reverse proxies to resell access and obfuscate sources.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks cloudtrail_threat_detection_llm_jacking

Recommendation

Apply least privilege to Bedrock; restrict Invoke* only to required roles and deny broadly via SCPs where unused. Enforce MFA and short-lived creds; rotate/remove exposed keys. Enable model invocation logging and budgets/quotas. Continuously monitor for Bedrock enumeration plus invoke bursts. Use defense in depth across identities and networks.

Remediation

Terraform

# SCP denying all Amazon Bedrock actions; attach it to the root/OU/account to halt LLM jacking
resource "aws_organizations_policy" "main" {
  name = "<example_resource_name>"
  type = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version   = "2012-10-17"
    Statement = [{
      Sid      = "DenyBedrock"
      Effect   = "Deny"
      Action   = "bedrock:*"   // CRITICAL: blocks all Bedrock APIs (prevents further suspicious activity)
      Resource = "*"            // CRITICAL: deny across all resources
    }]
  })
}

resource "aws_organizations_policy_attachment" "attach" {
  policy_id = aws_organizations_policy.main.id
  target_id = "<example_resource_id>"  // CRITICAL: attach to the affected account/OU/root to enforce the deny
}

NativeIaC

# CloudFormation SCP that blocks all Amazon Bedrock actions to stop LLM jacking
Resources:
  <example_resource_name>:
    Type: AWS::Organizations::Policy
    Properties:
      Name: <example_resource_name>
      Type: SERVICE_CONTROL_POLICY
      TargetIds:
        - "<example_resource_id>"  # CRITICAL: Attach SCP to the root/OU/account to enforce the deny
      Content:
        Version: "2012-10-17"
        Statement:
          - Sid: DenyBedrock
            Effect: Deny
            Action: "bedrock:*"  # CRITICAL: Denies all Bedrock APIs (Invoke/Converse/list/entitlements/etc.)
            Resource: "*"        # CRITICAL: Apply deny to all resources

Other

1In the AWS Console, go to Organizations > Policies > Service control policies
2Click Create policy
3Set Name to <example_resource_name>
4In Policy, paste a deny for Bedrock:
5{
6"Version": "2012-10-17",
7"Statement": [{"Sid":"DenyBedrock","Effect":"Deny","Action":"bedrock:*","Resource":"*"}]
8}
9Save the policy and click Attach
10Select the target (Root, OU, or the affected account ID <example_resource_id>) and attach the policy
11Wait for propagation; no further Bedrock calls will occur, and the finding will clear after the detection window elapses

Check Code

import json

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.cloudtrail.cloudtrail_client import (
    cloudtrail_client,
)

default_threat_detection_llm_jacking_actions = [
    "PutUseCaseForModelAccess",
    "PutFoundationModelEntitlement",
    "PutModelInvocationLoggingConfiguration",
    "CreateFoundationModelAgreement",
    "InvokeModel",
    "InvokeModelWithResponseStream",
    "GetUseCaseForModelAccess",
    "GetModelInvocationLoggingConfiguration",
    "GetFoundationModelAvailability",
    "ListFoundationModelAgreementOffers",
    "ListFoundationModels",
    "ListProvisionedModelThroughputs",
    "SearchAgreements",
    "AcceptAgreementRequest",
]


class cloudtrail_threat_detection_llm_jacking(Check):
    def execute(self):
        findings = []
        threshold = cloudtrail_client.audit_config.get(
            "threat_detection_llm_jacking_threshold", 0.4
        )
        threat_detection_minutes = cloudtrail_client.audit_config.get(
            "threat_detection_llm_jacking_minutes", 1440
        )
        llm_jacking_actions = cloudtrail_client.audit_config.get(
            "threat_detection_llm_jacking_actions",
            default_threat_detection_llm_jacking_actions,
        )
        potential_llm_jacking = {}
        found_potential_llm_jacking = False
        multiregion_trail = None
        # Check if any trail is multi-region so we only need to check once
        for trail in cloudtrail_client.trails.values():
            if trail.is_multiregion:
                multiregion_trail = trail
                break
        trails_to_scan = (
            cloudtrail_client.trails.values()
            if not multiregion_trail
            else [multiregion_trail]
        )
        for trail in trails_to_scan:
            for event_name in llm_jacking_actions:
                for event_log in cloudtrail_client._lookup_events(
                    trail=trail,
                    event_name=event_name,
                    minutes=threat_detection_minutes,
                ):
                    event_log = json.loads(event_log["CloudTrailEvent"])
                    if (
                        "arn" in event_log["userIdentity"]
                    ):  # Ignore event logs without ARN since they are AWS services
                        if (
                            event_log["userIdentity"]["arn"],
                            event_log["userIdentity"]["type"],
                        ) not in potential_llm_jacking:
                            potential_llm_jacking[
                                (
                                    event_log["userIdentity"]["arn"],
                                    event_log["userIdentity"]["type"],
                                )
                            ] = set()
                        potential_llm_jacking[
                            (
                                event_log["userIdentity"]["arn"],
                                event_log["userIdentity"]["type"],
                            )
                        ].add(event_name)

        for aws_identity, actions in potential_llm_jacking.items():
            identity_threshold = round(len(actions) / len(llm_jacking_actions), 2)
            aws_identity_type = aws_identity[1]
            aws_identity_arn = aws_identity[0]
            if len(actions) / len(llm_jacking_actions) > threshold:
                found_potential_llm_jacking = True
                report = Check_Report_AWS(
                    metadata=self.metadata(), resource=cloudtrail_client.trails
                )
                report.region = cloudtrail_client.region
                report.resource_id = aws_identity_arn.split("/")[-1]
                report.resource_arn = aws_identity_arn
                report.status = "FAIL"
                report.status_extended = f"Potential LLM Jacking attack detected from AWS {aws_identity_type} {aws_identity_arn.split('/')[-1]} with a threshold of {identity_threshold}."
                findings.append(report)
        if not found_potential_llm_jacking:
            report = Check_Report_AWS(
                metadata=self.metadata(), resource=cloudtrail_client.trails
            )
            report.region = cloudtrail_client.region
            report.resource_id = cloudtrail_client.audited_account
            report.resource_arn = cloudtrail_client._get_trail_arn_template(
                cloudtrail_client.region
            )
            report.status = "PASS"
            report.status_extended = "No potential LLM Jacking attack detected."
            findings.append(report)
        return findings