From 5b8eb5bcc302afbdb8f0b30b29ac11a26b95a86a Mon Sep 17 00:00:00 2001 From: Kay Date: Fri, 20 Sep 2024 20:37:29 +1000 Subject: [PATCH] Added Bedrock/cdk/guardrail/ - cdk for deploying Bedrock Guardrail (#684) --- .github/workflows/bedrock-build.yml | 45 +++++ Bedrock/cdk/guardrail/Makefile | 44 +++++ Bedrock/cdk/guardrail/app.py | 36 ++++ Bedrock/cdk/guardrail/bedrock_guardrail.py | 173 ++++++++++++++++++ Bedrock/cdk/guardrail/cdk.json | 61 ++++++ Bedrock/cdk/guardrail/environment/dev.yml | 81 ++++++++ Bedrock/cdk/guardrail/requirements.txt | 2 + Bedrock/cdk/guardrail/tests/test_guardrail.py | 95 ++++++++++ CHANGELOG.md | 6 + QuickSight/README.md | 20 +- 10 files changed, 553 insertions(+), 10 deletions(-) create mode 100644 .github/workflows/bedrock-build.yml create mode 100644 Bedrock/cdk/guardrail/Makefile create mode 100644 Bedrock/cdk/guardrail/app.py create mode 100644 Bedrock/cdk/guardrail/bedrock_guardrail.py create mode 100644 Bedrock/cdk/guardrail/cdk.json create mode 100644 Bedrock/cdk/guardrail/environment/dev.yml create mode 100644 Bedrock/cdk/guardrail/requirements.txt create mode 100644 Bedrock/cdk/guardrail/tests/test_guardrail.py diff --git a/.github/workflows/bedrock-build.yml b/.github/workflows/bedrock-build.yml new file mode 100644 index 0000000..e3be782 --- /dev/null +++ b/.github/workflows/bedrock-build.yml @@ -0,0 +1,45 @@ +name: Bedrock - Build +run-name: Test IaC @ ${{ github.ref_name }} + +on: + push: + paths: + - .github/workflows/bedrock-build.yml + - Bedrock/cdk/** + +concurrency: + cancel-in-progress: true + group: ${{ github.workflow }} + +defaults: + run: + shell: bash + working-directory: Bedrock/cdk/guardrail + +jobs: + bedrock-guardrail: + name: Test Bedrock Guardrail IaC + runs-on: ubuntu-latest + env: + ENV_STAGE: dev + steps: + - uses: actions/checkout@v4 + + - run: make lint-python + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Set up aws-cdk + run: make install-cdk + + - name: Print deployment environment + run: | + echo "INFO: cdk version: $(cdk --version)" + echo "INFO: node version: $(node --version)" + echo "INFO: npm version: $(npm --version)" + echo "INFO: python3 version: $(python3 --version)" + + - name: Run cdk synth + run: make synth-guardrail diff --git a/Bedrock/cdk/guardrail/Makefile b/Bedrock/cdk/guardrail/Makefile new file mode 100644 index 0000000..4981046 --- /dev/null +++ b/Bedrock/cdk/guardrail/Makefile @@ -0,0 +1,44 @@ +export AWS_DEFAULT_REGION ?= ap-southeast-2 +export CDK_DEFAULT_REGION ?= ap-southeast-2 +export ENV_STAGE ?= dev + +APP_NAME=$(shell grep -m 1 AppName environment/$(ENV_STAGE).yml | cut -c 10-) + +install-cdk: + npm install -g aws-cdk + python3 -m pip install -U pip + pip3 install -r requirements.txt + +synth-guardrail: + cdk synth $(APP_NAME)-BedrockGuardrail -c env=$(ENV_STAGE) + +diff-guardrail: + cdk diff $(APP_NAME)-BedrockGuardrail -c env=$(ENV_STAGE) + +deploy-guardrail: + cdk deploy $(APP_NAME)-BedrockGuardrail -c env=$(ENV_STAGE) $(APP_NAME) --require-approval never + +destroy-guardrail: + cdk destroy $(APP_NAME)-BedrockGuardrail -f -c env=$(ENV_STAGE) + +test-cdk: + pip3 install -r requirements-dev.txt && \ + python3 -m pytest . + +test-code: + python3 tests/test_guardrail.py + +pre-commit: format-python lint-python lint-yaml + +format-python: + black --line-length=100 **.py */**.py + +lint-python: + pip3 install flake8 + flake8 --ignore E501,F541,W503,W605 **.py */**.py + +lint-yaml: + yamllint -c .github/linters/.yaml-lint.yml -f parsable . + +clean: + rm -rf cdk.out __pycache__ diff --git a/Bedrock/cdk/guardrail/app.py b/Bedrock/cdk/guardrail/app.py new file mode 100644 index 0000000..62b6291 --- /dev/null +++ b/Bedrock/cdk/guardrail/app.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +from os.path import dirname, join, realpath + +import yaml +from aws_cdk import App, Environment, Tags +from bedrock_guardrail import BedrockGuardrailStack + +ENV_DIR = join(dirname(realpath(__file__)), "environment") + + +def main(): + app = App() + + ENV_NAME = app.node.try_get_context("env") or "dev" + + with open(join(ENV_DIR, f"{ENV_NAME}.yml"), "r") as stream: + yaml_data = yaml.safe_load(stream) + config = yaml_data if yaml_data is not None else {} + + app_name = config["AppName"] + + stack = BedrockGuardrailStack( + scope=app, + id=f"{app_name}-BedrockGuardrail", + config=config, + env=Environment(account=config["Account"], region=config["Region"]), + ) + + for key, value in config["Tags"].items(): + Tags.of(stack).add(key, value) + + app.synth() + + +if __name__ == "__main__": + main() diff --git a/Bedrock/cdk/guardrail/bedrock_guardrail.py b/Bedrock/cdk/guardrail/bedrock_guardrail.py new file mode 100644 index 0000000..4e92cde --- /dev/null +++ b/Bedrock/cdk/guardrail/bedrock_guardrail.py @@ -0,0 +1,173 @@ +from aws_cdk.aws_bedrock import CfnGuardrail, CfnGuardrailVersion +from aws_cdk import CfnOutput, Stack +from constructs import Construct + + +class BedrockGuardrailStack(Stack): + def __init__(self, scope: Construct, id: str, config: dict, **kwargs) -> None: + super().__init__(scope, id, **kwargs) + self.config = config + app_name = self.config["AppName"].lower() + + params = { + "blocked_input_messaging": self.config["BedrockGuardrail"]["blocked_input_messaging"], + "blocked_outputs_messaging": self.config["BedrockGuardrail"][ + "blocked_outputs_messaging" + ], + } + + # (1) Content filter policies + content_policy_config = self.create_content_policy_config() + if content_policy_config: + params["content_policy_config"] = content_policy_config + + # (2) Contextual grounding policies + contextual_grounding_policy_config = self.create_contextual_grounding_policy_config() + if contextual_grounding_policy_config: + params["contextual_grounding_policy_config"] = contextual_grounding_policy_config + + # (3) Sensitive information policies + sensitive_information_policy_config = self.create_sensitive_information_policy_config() + if sensitive_information_policy_config: + params["sensitive_information_policy_config"] = sensitive_information_policy_config + + # (4) Topic policies + topic_policy_config = self.create_topic_policy_config() + if topic_policy_config: + params["topic_policy_config"] = topic_policy_config + + # (5) Word filters + word_policy_config = self.create_word_policy_config() + if word_policy_config: + params["word_policy_config"] = word_policy_config + + guardrail = CfnGuardrail( + self, + id=f"{app_name}-guardrail", + name=f"{app_name}-guardrail", + description=f"{app_name} Guardrail", + # kms_key_arn="kmsKeyArn", TODO + **params, + ) + + guardrail_version = CfnGuardrailVersion( + self, + id=f"{app_name}-guardrail-version", + description=f"{app_name} Guardrail Version", + guardrail_identifier=guardrail.attr_guardrail_id, + ) + CfnOutput(self, "GuardrailIdentifier", value=guardrail.attr_guardrail_id) + CfnOutput(self, "GuardrailVersion", value=guardrail_version.attr_version) + + def create_content_policy_config(self) -> CfnGuardrail.ContentPolicyConfigProperty: + """ + Adjust filter strengths to block input prompts or model responses containing harmful content. + """ + filters_config = [ + CfnGuardrail.ContentFilterConfigProperty( + input_strength=v["input_strength"], + output_strength=v["output_strength"], + type=k, + ) + for k, v in self.config["BedrockGuardrail"]["content_policy_config"].items() + ] + return CfnGuardrail.ContentPolicyConfigProperty(filters_config=filters_config) + + def create_contextual_grounding_policy_config( + self, + ) -> CfnGuardrail.ContextualGroundingPolicyConfigProperty: + """ + Use contextual grounding check to filter hallucinations in responses + """ + filters_config = [ + CfnGuardrail.ContextualGroundingFilterConfigProperty( + threshold=v["threshold"], + type=k, + ) + for k, v in self.config["BedrockGuardrail"][ + "contextual_grounding_policy_config" + ].items() + ] + return CfnGuardrail.ContextualGroundingPolicyConfigProperty(filters_config=filters_config) + + def create_sensitive_information_policy_config( + self, + ) -> CfnGuardrail.SensitiveInformationPolicyConfigProperty: + """ + Block or mask sensitive information such as personally identifiable information (PII) + or custom regex in user inputs and model responses. + """ + params = {} + pii_entities_config = [ + CfnGuardrail.PiiEntityConfigProperty(action=v, type=k) + for k, v in self.config["BedrockGuardrail"]["sensitive_information_policy_config"][ + "pii_entities_config" + ].items() + ] + if pii_entities_config: + params["pii_entities_config"] = pii_entities_config + + regexes_config = [ + CfnGuardrail.RegexConfigProperty( + action=item["action"], + name=item["name"], + pattern=item["pattern"], + description=item.get("description", ""), + ) + for item in self.config["BedrockGuardrail"]["sensitive_information_policy_config"][ + "regexes_config" + ] + ] + if regexes_config: + params["regexes_config"] = regexes_config + + if pii_entities_config or regexes_config: + return CfnGuardrail.SensitiveInformationPolicyConfigProperty(**params) + return None + + def create_topic_policy_config(self) -> CfnGuardrail.TopicPolicyConfigProperty: + """ + Block denied topics to remove harmful content + """ + topics_config = [ + CfnGuardrail.TopicConfigProperty( + definition=item["definition"], + name=item["name"], + type="DENY", + examples=item.get("examples", []), + ) + for item in self.config["BedrockGuardrail"]["topic_policy_config"] + ] + if topics_config: + return CfnGuardrail.TopicPolicyConfigProperty(topics_config=topics_config) + return None + + def create_word_policy_config(self) -> CfnGuardrail.WordPolicyConfigProperty: + """ + Configure filters to block undesirable words, phrases, and profanity. Such words can include offensive terms, competitor names etc. + """ + params = {} + + managed_word_lists_config = ( + [CfnGuardrail.ManagedWordsConfigProperty(type="PROFANITY")] + if self.config["BedrockGuardrail"]["word_policy_config"].get( + "managed_word_lists_config" + ) + == "PROFANITY" + else [] + ) + if managed_word_lists_config: + params["managed_word_lists_config"] = managed_word_lists_config + + words_config = [ + CfnGuardrail.WordConfigProperty(text=item) + for item in self.config["BedrockGuardrail"]["word_policy_config"].get( + "words_config", [] + ) + ] + if words_config: + params["words_config"] = words_config + + if managed_word_lists_config or words_config: + return CfnGuardrail.WordPolicyConfigProperty(**params) + return None diff --git a/Bedrock/cdk/guardrail/cdk.json b/Bedrock/cdk/guardrail/cdk.json new file mode 100644 index 0000000..87d33bf --- /dev/null +++ b/Bedrock/cdk/guardrail/cdk.json @@ -0,0 +1,61 @@ +{ + "app": "python3 app.py", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "requirements*.txt", + "**/__init__.py", + "**/__pycache__", + "tests" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk-containers/ecs-service-extensions:enableDefaultLogDriver": true, + "@aws-cdk/aws-ec2:uniqueImdsv2TemplateName": true, + "@aws-cdk/aws-ecs:arnFormatIncludesClusterName": true, + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-codepipeline:crossAccountKeyAliasStackSafeResourceName": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-sns-subscriptions:restrictSqsDescryption": true, + "@aws-cdk/aws-apigateway:disableCloudWatchRole": true, + "@aws-cdk/core:enablePartitionLiterals": true, + "@aws-cdk/aws-events:eventsTargetQueueSameAccount": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ecs:disableExplicitDeploymentControllerForCircuitBreaker": true, + "@aws-cdk/aws-iam:importedRoleStackSafeDefaultPolicyName": true, + "@aws-cdk/aws-s3:serverAccessLogsUseBucketPolicy": true, + "@aws-cdk/aws-route53-patters:useCertificate": true, + "@aws-cdk/customresources:installLatestAwsSdkDefault": false, + "@aws-cdk/aws-rds:databaseProxyUniqueResourceName": true, + "@aws-cdk/aws-codedeploy:removeAlarmsFromDeploymentGroup": true, + "@aws-cdk/aws-apigateway:authorizerChangeDeploymentLogicalId": true, + "@aws-cdk/aws-ec2:launchTemplateDefaultUserData": true, + "@aws-cdk/aws-secretsmanager:useAttachedSecretResourcePolicyForSecretTargetAttachments": true, + "@aws-cdk/aws-redshift:columnId": true, + "@aws-cdk/aws-stepfunctions-tasks:enableEmrServicePolicyV2": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/aws-apigateway:requestValidatorUniqueId": true, + "@aws-cdk/aws-kms:aliasNameRef": true, + "@aws-cdk/aws-autoscaling:generateLaunchTemplateInsteadOfLaunchConfig": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-efs:denyAnonymousAccess": true, + "@aws-cdk/aws-opensearchservice:enableOpensearchMultiAzWithStandby": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true, + "@aws-cdk/aws-efs:mountTargetOrderInsensitiveLogicalId": true, + "@aws-cdk/aws-rds:auroraClusterChangeScopeOfInstanceParameterGroupWithEachParameters": true, + "@aws-cdk/aws-appsync:useArnForSourceApiAssociationIdentifier": true, + "@aws-cdk/aws-rds:preventRenderingDeprecatedCredentials": true, + "@aws-cdk/aws-codepipeline-actions:useNewDefaultBranchForCodeCommitSource": true, + "@aws-cdk/aws-cloudwatch-actions:changeLambdaPermissionLogicalIdForLambdaAction": true, + "@aws-cdk/aws-codepipeline:crossAccountKeysDefaultValueToFalse": true, + "@aws-cdk/aws-codepipeline:defaultPipelineTypeToV2": true, + "@aws-cdk/aws-kms:reduceCrossAccountRegionPolicyScope": true, + "@aws-cdk/aws-eks:nodegroupNameAttribute": true + } +} diff --git a/Bedrock/cdk/guardrail/environment/dev.yml b/Bedrock/cdk/guardrail/environment/dev.yml new file mode 100644 index 0000000..8b6a25d --- /dev/null +++ b/Bedrock/cdk/guardrail/environment/dev.yml @@ -0,0 +1,81 @@ +AppName: Test +Account: "123456789012" +Region: ap-southeast-2 +BedrockGuardrail: + # The message to return when the guardrail blocks a prompt + blocked_input_messaging: "This prompt is blocked by Guardrail." + # The message to return when the guardrail blocks a model response + blocked_outputs_messaging: "This model response is blocked by Guardrail." + content_policy_config: + # Adjust filter strengths to block input prompts or model responses containing harmful content. + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-content-filters.html + # Allowed values: NONE | LOW | MEDIUM | HIGH + SEXUAL: + input_strength: HIGH + output_strength: HIGH + VIOLENCE: + input_strength: HIGH + output_strength: HIGH + HATE: + input_strength: MEDIUM + output_strength: HIGH + INSULTS: + input_strength: LOW + output_strength: HIGH + MISCONDUCT: + input_strength: LOW + output_strength: HIGH + PROMPT_ATTACK: + input_strength: HIGH + output_strength: NONE # Must be NONE for response + contextual_grounding_policy_config: + # Use contextual grounding check to filter hallucinations in responses + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-contextual-grounding-check.html + # Threshold: between 0 and 0.99. 1 is invalid as that will block all content. + GROUNDING: + threshold: 0.8 + RELEVANCE: + threshold: 0.7 + sensitive_information_policy_config: + # Block or mask sensitive information such as PII or custom regex in user inputs and model responses + # Types supported - https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-bedrock-guardrail-piientityconfig.html + # Actions Allowed values: BLOCK | ANONYMIZE + pii_entities_config: + # Only a subset of PII entities are shown here + ADDRESS: BLOCK + DRIVER_ID: BLOCK + EMAIL: ANONYMIZE + PASSWORD: BLOCK + PHONE: ANONYMIZE + LICENSE_PLATE: BLOCK + VEHICLE_IDENTIFICATION_NUMBER: ANONYMIZE + CREDIT_DEBIT_CARD_CVV: BLOCK + CREDIT_DEBIT_CARD_EXPIRY: BLOCK + CREDIT_DEBIT_CARD_NUMBER: BLOCK + INTERNATIONAL_BANK_ACCOUNT_NUMBER: BLOCK + PIN: BLOCK + SWIFT_CODE: BLOCK + AWS_ACCESS_KEY: BLOCK + AWS_SECRET_KEY: BLOCK + regexes_config: [] + # - name: xx + # description: xx + # action: BLOCK | ANONYMIZE + # pattern: "xx" + topic_policy_config: [] + # Block denied topics to remove harmful content + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-topic-policy.html + # Example + # - name: Investment Advice + # definition: Investment advice is inquiries, guidance, or recommendations about the management or allocation of funds or assets with the goal of generating returns or achieving specific financial objectives + # examples: + # - Is investing in the stocks better than bonds? + # - Should I invest in gold? + word_policy_config: + # Configure filters to block undesirable words, phrases, and profanity. Such words can include offensive terms, competitor names etc. + # https://docs.aws.amazon.com/bedrock/latest/userguide/guardrails-word-filters.html + managed_word_lists_config: PROFANITY + words_config: [] # list of words to be blocked +Tags: + CostCentre: TODO + Project: TODO diff --git a/Bedrock/cdk/guardrail/requirements.txt b/Bedrock/cdk/guardrail/requirements.txt new file mode 100644 index 0000000..1515000 --- /dev/null +++ b/Bedrock/cdk/guardrail/requirements.txt @@ -0,0 +1,2 @@ +aws-cdk-lib==2.158.0 +constructs==10.3.0 diff --git a/Bedrock/cdk/guardrail/tests/test_guardrail.py b/Bedrock/cdk/guardrail/tests/test_guardrail.py new file mode 100644 index 0000000..ef04f15 --- /dev/null +++ b/Bedrock/cdk/guardrail/tests/test_guardrail.py @@ -0,0 +1,95 @@ +""" +Examples to show how to use Bedrock Guardrail with Converse API. +""" +import json + +import boto3 +from botocore.exceptions import ClientError + +# The ID and version of the guardrail. +GUARDRAIL_ID = "TODO" +GUARDRAIL_VERSION = "1" + +guardrail_config = { + "guardrailIdentifier": GUARDRAIL_ID, + "guardrailVersion": GUARDRAIL_VERSION, + "trace": "enabled", +} + +model_id = "anthropic.claude-3-sonnet-20240229-v1:0" + +system_prompts = [ + { + "text": "You are an app that supports customers of an e-commerce site. Do not return any personal information of customers." + } +] + + +def converse_api(messages: list): + try: + bedrock_client = boto3.client(service_name="bedrock-runtime") + + print(f"Input prompt: {messages}") + # Inference parameters to use. + temperature = 0.5 + top_k = 200 + # Base inference parameters to use. + inference_config = {"temperature": temperature} + # Additional inference parameters to use. + additional_model_fields = {"top_k": top_k} + + # Send the message. + response = bedrock_client.converse( + modelId=model_id, + messages=messages, + system=system_prompts, + inferenceConfig=inference_config, + additionalModelRequestFields=additional_model_fields, + guardrailConfig=guardrail_config, + ) + + print(f'Stop reason: {response["stopReason"]}') + if response["stopReason"] == "guardrail_intervened": + trace = response["trace"] + print("Guardrail trace:") + print(json.dumps(trace["guardrail"], indent=2)) + + output_message = response["output"]["message"] + for content in output_message["content"]: + print(f"Text: {content['text']}") + + except ClientError as err: + message = err.response["Error"]["Message"] + print(f"A client error occured: {message}") + + +def main(): + print(f"Generating messages with model {model_id}") + + user_msg_1 = {"role": "user", "content": [{"text": "What is my customer account password?"}]} + user_msg_2 = {"role": "user", "content": [{"text": "What is my customer account balance?"}]} + messages = [user_msg_1, user_msg_2] + + print("Testing guardrail for user role") + for message in messages: + converse_api([message]) + print("----------------------------------") + + asst_msg_1 = {"role": "assistant", "content": [{"text": "Your password is 123456."}]} + asst_msg_2 = { + "role": "assistant", + "content": [{"text": "Your registered address is 1234 Dummy St, VIC, 1000."}], + } + messages_list = [ + [user_msg_1, asst_msg_1], + [user_msg_2, asst_msg_2], + ] + + print("Testing guardrail for assistant role") + for messages in messages_list: + converse_api(messages) + print("----------------------------------") + + +if __name__ == "__main__": + main() diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aebdc4..56286fa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ All notable changes to this project will be documented in this file. +## 2024-09-20 + +### + + * Added [Bedrock/cdk/guardrail/](Bedrock/cdk/guardrail/) - cdk for deploying Bedrock Guardrail + ## 2024-05-23 ### Added diff --git a/QuickSight/README.md b/QuickSight/README.md index 04c7385..cae063e 100644 --- a/QuickSight/README.md +++ b/QuickSight/README.md @@ -1,10 +1,10 @@ -# QuickSight - -Jump to -- [Useful Libs and Tools](#useful-libs-and-tools) - ---- - -## Useful Libs and Tools - -- [awslabs/cid-framework](https://github.com/awslabs/cid-framework) - Cloud Intelligence Dashboards +# QuickSight + +Jump to +- [Useful Libs and Tools](#useful-libs-and-tools) + +--- + +## Useful Libs and Tools + +- [awslabs/cid-framework](https://github.com/awslabs/cid-framework) - Cloud Intelligence Dashboards