diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 71152867..60c40cc3 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -34,34 +34,22 @@ jobs: echo "Changed files:" echo "$FILES" - declare -A ROLE_MAP=( + declare -A ROLE_MAP=( ["operation-team-account"]="ROLE_ARN_OPERATION" ["identity-team-account"]="ROLE_ARN_IDENTITY" - ["prod-team-account"]="ROLE_ARN_PROD" - ["dev-team-account"]="ROLE_ARN_DEV" - ["security-team-account"]="ROLE_ARN_SECURITY" - ["stage-team-account"]="ROLE_ARN_STAGE" ["management-team-account"]="ROLE_ARN_MANAGEMENT" ) TMP_FILE=$(mktemp) - for FILE in $FILES; do DIR=$(dirname "$FILE") TOP_DIR=$(echo $DIR | cut -d/ -f1) ROLE_KEY="${ROLE_MAP[$TOP_DIR]}" if [ -n "$ROLE_KEY" ]; then - if [ "$DIR" == "$TOP_DIR" ]; then - TF_COUNT=$(find "$DIR" -maxdepth 1 -name '*.tf' | wc -l) - if [ "$TF_COUNT" -gt 0 ]; then - echo "$DIR|$ROLE_KEY" >> $TMP_FILE - fi - else - TF_COUNT=$(find "$DIR" -maxdepth 1 -name '*.tf' | wc -l) - if [ "$TF_COUNT" -gt 0 ]; then - echo "$DIR|$ROLE_KEY" >> $TMP_FILE - fi + TF_COUNT=$(find "$DIR" -maxdepth 1 -name '*.tf' | wc -l) + if [ "$TF_COUNT" -gt 0 ]; then + echo "$DIR|$ROLE_KEY" >> $TMP_FILE fi fi done @@ -84,10 +72,8 @@ jobs: done <<< "$UNIQUE_LINES" MATRIX_JSON="$MATRIX_JSON]" - echo "Final JSON matrix:" echo "$MATRIX_JSON" - echo "matrix=$MATRIX_JSON" >> $GITHUB_OUTPUT terraform-ci: @@ -104,6 +90,7 @@ jobs: INFRACOST_API_KEY: ${{ secrets.INFRACOST_API_KEY }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} INFRACOST_TERRAFORM_CLI_WRAPPER: false + TF_VAR_slack_webhook_url: ${{ secrets.TF_VAR_slack_webhook_url }} steps: - name: Checkout Code @@ -152,29 +139,41 @@ jobs: PLAN_TXT=plan.txt PLAN_JSON=plan.json - # Run terraform plan - terraform plan -no-color -out=$PLAN_FILE > /dev/null 2> plan_error.txt || echo "PLAN_FAILED=true" >> $GITHUB_ENV + if terraform plan -no-color -out=$PLAN_FILE > /dev/null 2> plan_error.txt; then + echo "PLAN_FAILED=false" >> $GITHUB_ENV + terraform show -no-color $PLAN_FILE > $PLAN_TXT + terraform show -json $PLAN_FILE > $PLAN_JSON || true + else + echo "PLAN_FAILED=true" >> $GITHUB_ENV + echo "Plan failed" > $PLAN_TXT + echo "{}" > $PLAN_JSON + fi - # Show plan text output - terraform show -no-color $PLAN_FILE > $PLAN_TXT 2>/dev/null || echo "Plan failed" > $PLAN_TXT + # 디버깅용 출력 + echo "::group::Raw terraform show output" + cat $PLAN_TXT || echo "(empty)" + echo "::endgroup::" - # Remove ANSI color codes - cat $PLAN_TXT | \ - sed 's/`/\\`/g' | \ - tr -d '\r' | \ - sed -r "s/\x1B\[[0-9;]*[JKmsu]//g" \ - > cleaned_plan.txt + sed 's/`/\\`/g' $PLAN_TXT | tr -d '\r' | sed -r "s/\x1B\[[0-9;]*[JKmsu]//g" > cleaned_plan.txt PLAN_CONTENT=$(cat cleaned_plan.txt) + PLAN_ERROR=$(cat plan_error.txt || echo "No error captured") + + if [ -z "$PLAN_CONTENT" ]; then + PLAN_CONTENT="(no changes or output empty)" + fi - # Save JSON plan for infracost - terraform show -json $PLAN_FILE > $PLAN_JSON || true + if [ -z "$PLAN_ERROR" ]; then + PLAN_ERROR="(no errors)" + fi - # Output plan content for PR comment { echo "PLAN_CONTENT<> $GITHUB_OUTPUT working-directory: ${{ matrix.dir }} @@ -198,6 +197,11 @@ jobs: ${{ steps.plan.outputs.PLAN_CONTENT }} ``` + ### Plan Error (if any) + ``` + ${{ steps.plan.outputs.PLAN_ERROR }} + ``` + - name: Setup Infracost uses: infracost/actions/setup@v2 @@ -213,4 +217,4 @@ jobs: uses: infracost/actions/comment@v1 with: path: ${{ matrix.dir }}/infracost.json - behavior: update \ No newline at end of file + behavior: update diff --git a/.gitignore b/.gitignore index 14b77fbf..08d00230 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ ehthumbs.db # Windows *.tmp # 임시 파일 # VSCode 설정 (선택사항) -.vscode/ \ No newline at end of file +.vscode/ + +# OpenSearch alert 생성 시 생기는 임시 파일 +modules/opensearch/slack_response.json \ No newline at end of file diff --git a/management-team-account/monitoring/main.tf b/management-team-account/monitoring/main.tf new file mode 100644 index 00000000..52778c18 --- /dev/null +++ b/management-team-account/monitoring/main.tf @@ -0,0 +1,39 @@ +terraform { + required_version = ">= 1.1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + + backend "s3" { + bucket = "cloudfence-management-state" + key = "cloudtrail/terraform.tfstate" + region = "ap-northeast-2" + encrypt = true + dynamodb_table = "tfstate-management-lock" + } +} + +provider "aws" { + region = var.aws_region +} + +data "terraform_remote_state" "operation" { + backend = "s3" + config = { + bucket = "cloudfence-operation-state" + key = "monitoring/terraform.tfstate" + region = "ap-northeast-2" + } +} + +data "aws_caller_identity" "current" {} + +module "cloudtrail" { + source = "../../modules/cloudtrail_org" + org_trail_name = var.org_trail_name + cloudtrail_bucket_name = data.terraform_remote_state.operation.outputs.bucket_name + cloudtrail_kms_key_arn = data.terraform_remote_state.operation.outputs.kms_key_arn +} \ No newline at end of file diff --git a/management-team-account/monitoring/variables.tf b/management-team-account/monitoring/variables.tf new file mode 100644 index 00000000..27580262 --- /dev/null +++ b/management-team-account/monitoring/variables.tf @@ -0,0 +1,11 @@ +variable "aws_region" { + description = "AWS region" + type = string + default = "ap-northeast-2" +} + +variable "org_trail_name" { + description = "Organization CloudTrail name" + type = string + default = "org-cloudtrail" +} \ No newline at end of file diff --git a/modules/cloudtrail_org/main.tf b/modules/cloudtrail_org/main.tf new file mode 100644 index 00000000..10598c43 --- /dev/null +++ b/modules/cloudtrail_org/main.tf @@ -0,0 +1,29 @@ +resource "aws_cloudtrail" "org" { + name = var.org_trail_name + is_organization_trail = true + is_multi_region_trail = true + include_global_service_events = true + enable_log_file_validation = true + enable_logging = true + + s3_bucket_name = var.cloudtrail_bucket_name + kms_key_id = var.cloudtrail_kms_key_arn + + event_selector { + read_write_type = "All" + include_management_events = true + + data_resource { + type = "AWS::S3::Object" + values = [ + "arn:aws:s3:::${var.cloudtrail_bucket_name}/AWSLogs/" + ] + } + } + + tags = { + Name = var.org_trail_name + Environment = "prod" + Owner = "security-team" + } +} \ No newline at end of file diff --git a/modules/cloudtrail_org/variables.tf b/modules/cloudtrail_org/variables.tf new file mode 100644 index 00000000..0133252c --- /dev/null +++ b/modules/cloudtrail_org/variables.tf @@ -0,0 +1,14 @@ +variable "org_trail_name" { + description = "Organization CloudTrail name" + type = string +} + +variable "cloudtrail_bucket_name" { + description = "S3 bucket name for CloudTrail logs (from operation account)" + type = string +} + +variable "cloudtrail_kms_key_arn" { + description = "KMS key ARN for CloudTrail SSE-KMS (from operation account)" + type = string +} diff --git a/modules/eventbridge_triggers/main.tf b/modules/eventbridge_triggers/main.tf new file mode 100644 index 00000000..4ab1dc64 --- /dev/null +++ b/modules/eventbridge_triggers/main.tf @@ -0,0 +1,30 @@ +resource "aws_cloudwatch_event_rule" "s3_object_created" { + name = "cloudtrail-s3-event-rule" + description = "Trigger Lambda on CloudTrail S3 delivery objects" + + event_pattern = jsonencode({ + source = ["aws.s3"] + detail = { + eventName = ["PutObject"] + requestParameters = { + bucketName = [var.bucket_name] + key = [{ prefix = "AWSLogs/" }] + } + } + }) + event_bus_name = "default" +} + +resource "aws_cloudwatch_event_target" "lambda" { + rule = aws_cloudwatch_event_rule.s3_object_created.name + target_id = "lambda-target" + arn = var.lambda_function_arn +} + +resource "aws_lambda_permission" "allow_eventbridge" { + statement_id = "AllowExecutionFromEventBridge" + action = "lambda:InvokeFunction" + function_name = var.lambda_function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.s3_object_created.arn +} \ No newline at end of file diff --git a/modules/eventbridge_triggers/outputs.tf b/modules/eventbridge_triggers/outputs.tf new file mode 100644 index 00000000..cf9967c0 --- /dev/null +++ b/modules/eventbridge_triggers/outputs.tf @@ -0,0 +1,3 @@ +output "event_rule_arn" { + value = aws_cloudwatch_event_rule.s3_object_created.arn +} \ No newline at end of file diff --git a/modules/eventbridge_triggers/variables.tf b/modules/eventbridge_triggers/variables.tf new file mode 100644 index 00000000..f7d96b7c --- /dev/null +++ b/modules/eventbridge_triggers/variables.tf @@ -0,0 +1,14 @@ +variable "bucket_name" { + description = "Name of the S3 bucket where CloudTrail logs are stored" + type = string +} + +variable "lambda_function_name" { + description = "Lambda function name to be triggered" + type = string +} + +variable "lambda_function_arn" { + description = "ARN of the Lambda function to trigger" + type = string +} \ No newline at end of file diff --git a/modules/lambda_alerting/index.js b/modules/lambda_alerting/index.js new file mode 100644 index 00000000..ea24e0d2 --- /dev/null +++ b/modules/lambda_alerting/index.js @@ -0,0 +1,189 @@ +const AWS = require("aws-sdk"); +const { Client } = require("@opensearch-project/opensearch"); +const createAwsOpensearchConnector = require("aws-opensearch-connector"); + +AWS.config.update({ region: process.env.AWS_REGION }); + +const rawEndpoint = process.env.OPENSEARCH_ENDPOINT; + +if (!rawEndpoint) { + throw new Error("Missing OPENSEARCH_ENDPOINT environment variable"); +} + +const endpoint = rawEndpoint.startsWith("https://") + ? rawEndpoint + : `https://${rawEndpoint}`; + +const client = new Client({ + ...createAwsOpensearchConnector(AWS.config), + node: endpoint, + ssl: { + rejectUnauthorized: false, + }, +}); + +exports.handler = async () => { + try { + const existingDestinations = await client.transport.request({ + method: "GET", + path: "/_plugins/_notifications/configs", + }); + + const existingSlack = existingDestinations.body.config_list.find( + (config) => config.config.name === "slack-destination" + ); + + let destinationId; + if (existingSlack) { + destinationId = existingSlack.config_id; + } else { + const destination = await client.transport.request({ + method: "POST", + path: "/_plugins/_notifications/configs", + body: { + config: { + name: "slack-destination", + description: "Slack alerts", + config_type: "slack", + is_enabled: true, + slack: { + url: process.env.SLACK_WEBHOOK_URL, + }, + }, + }, + }); + destinationId = destination.body.config_id; + } + + const monitorBody = { + type: "monitor", + name: "security_event_monitor", + enabled: true, + schedule: { + period: { + interval: 1, + unit: "MINUTES", + }, + }, + inputs: [ + { + search: { + indices: ["cloudtrail-logs-*"], + query: { + size: 1, + collapse: { + field: "eventID.keyword", + }, + query: { + bool: { + must: [ + { + terms: { + "eventName.keyword": [ + "DeleteUser", + "DeleteRole", + "DeleteLoginProfile", + "StopLogging", + "DeleteTrail", + "DeactivateMFADevice", + "DeleteVirtualMFADevice", + "AuthorizeSecurityGroupIngress", + "RevokeSecurityGroupIngress", + "AuthorizeSecurityGroupEgress", + "RevokeSecurityGroupEgress", + "AttachUserPolicy", + "DetachUserPolicy", + "PutUserPolicy", + "DeleteUserPolicy", + "CreatePolicy", + "DeletePolicy", + "RunInstances", + ], + }, + }, + { + range: { + eventTime: { + gte: "now-5m", + lt: "now", + }, + }, + }, + ], + }, + }, + }, + }, + }, + ], + triggers: [ + { + name: "security_event_trigger", + severity: "1", + condition: { + script: { + source: "ctx.results[0].hits.total.value > 0", + lang: "painless", + }, + }, + actions: [ + { + name: "slack_action", + destination_id: destinationId, + message_template: { + source: `{{#ctx.results.0.hits.hits}} +:rotating_light: *AWS Security Alert* + +*Event:* {{_source.eventName}} +*User ARN:* {{_source.userIdentity.arn}} +*Source IP:* {{_source.sourceIPAddress}} +*Region:* {{_source.awsRegion}} +*Account:* {{_source.userIdentity.accountId}} +*Time (UTC):* {{_source.eventTime}} +{{/ctx.results.0.hits.hits}}`, + }, + }, + ], + }, + ], + }; + + await client.transport.request({ + method: "POST", + path: "/_plugins/_alerting/monitors", + body: monitorBody, + }); + + const indexPatterns = [ + { + id: "index-pattern:cloudtrail-logs", + title: "cloudtrail-logs-*", + }, + { + id: "index-pattern:waf-logs", + title: "waf-logs-*", + }, + ]; + + for (const pattern of indexPatterns) { + await client.transport.request({ + method: "POST", + path: `/.kibana/_doc/${pattern.id}`, + body: { + type: "index-pattern", + "index-pattern": { + title: pattern.title, + timeFieldName: "@timestamp", + }, + }, + }); + } + + console.log("✅ Alerting rules and index patterns successfully created."); + } catch (error) { + console.error("❌ Failed to create alerting rules or index patterns:"); + console.error("Message:", error.message); + console.error("Meta:", JSON.stringify(error.meta?.body || {}, null, 2)); + throw error; + } +}; diff --git a/modules/lambda_alerting/init-alerting.zip b/modules/lambda_alerting/init-alerting.zip new file mode 100644 index 00000000..4897db31 Binary files /dev/null and b/modules/lambda_alerting/init-alerting.zip differ diff --git a/modules/lambda_alerting/main.tf b/modules/lambda_alerting/main.tf new file mode 100644 index 00000000..91f60145 --- /dev/null +++ b/modules/lambda_alerting/main.tf @@ -0,0 +1,68 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "lambda_exec" { + name = "${var.function_name}-exec-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17", + Statement = [{ + Effect = "Allow", + Principal = { Service = "lambda.amazonaws.com" }, + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "${var.function_name}-policy" + role = aws_iam_role.lambda_exec.id + policy = jsonencode({ + Version = "2012-10-17", + Statement = [ + # CloudWatch 권한 + { + Effect = "Allow", + Action = [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.function_name}:*" + }, + # OpenSearch POST 권한 + { + Effect = "Allow", + Action = [ + "es:ESHttpPost", + "es:ESHttpPut", + "es:ESHttpGet" + ], + Resource = "arn:aws:es:${var.aws_region}:${data.aws_caller_identity.current.account_id}:domain/${var.domain_name}/*" + } + ] + }) +} + +resource "aws_lambda_function" "alerting_setup" { + function_name = var.function_name + handler = var.handler + runtime = var.runtime + role = aws_iam_role.lambda_exec.arn + timeout = 30 + filename = "${path.module}/${var.zip_file_path}" + source_code_hash = filebase64sha256("${path.module}/${var.zip_file_path}") + + environment { + variables = { + OPENSEARCH_ENDPOINT = var.opensearch_endpoint + SLACK_WEBHOOK_URL = var.slack_webhook_url + } + } +} + +output "lambda_function_name" { + value = aws_lambda_function.alerting_setup.function_name +} + +output "lambda_function_arn" { + value = aws_lambda_function.alerting_setup.arn +} \ No newline at end of file diff --git a/modules/lambda_alerting/package-lock.json b/modules/lambda_alerting/package-lock.json new file mode 100644 index 00000000..b0aa6203 --- /dev/null +++ b/modules/lambda_alerting/package-lock.json @@ -0,0 +1,615 @@ +{ + "name": "init-opensearch-alerting", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "init-opensearch-alerting", + "version": "1.0.0", + "dependencies": { + "@opensearch-project/opensearch": "^2.1.0", + "aws-opensearch-connector": "^1.1.0", + "aws-sdk": "^2.1340.0" + } + }, + "node_modules/@opensearch-project/opensearch": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz", + "integrity": "sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==", + "dependencies": { + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "json11": "^2.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-opensearch-connector": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aws-opensearch-connector/-/aws-opensearch-connector-1.2.0.tgz", + "integrity": "sha512-Jex72kdNtkpXdBFkA8n/VkWqI+S0AjqY6xGN8I8qA7W2CAjppBEh6PVRER1zLa/Q+/dqxou15uZ5Qz6NFfmqFQ==", + "dependencies": { + "aws4": "^1.11.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "@opensearch-project/opensearch": ">=1.0.0" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/json11": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json11/-/json11-2.0.2.tgz", + "integrity": "sha512-HIrd50UPYmP6sqLuLbFVm75g16o0oZrVfxrsY0EEys22klz8mRoWlX9KAEDOSOR9Q34rcxsyC8oDveGrCz5uLQ==", + "bin": { + "json11": "dist/cli.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/modules/lambda_alerting/package.json b/modules/lambda_alerting/package.json new file mode 100644 index 00000000..81b93cdb --- /dev/null +++ b/modules/lambda_alerting/package.json @@ -0,0 +1,9 @@ +{ + "name": "init-opensearch-alerting", + "version": "1.0.0", + "dependencies": { + "aws-sdk": "^2.1340.0", + "@opensearch-project/opensearch": "^2.1.0", + "aws-opensearch-connector": "^1.1.0" + } +} diff --git a/modules/lambda_alerting/variables.tf b/modules/lambda_alerting/variables.tf new file mode 100644 index 00000000..b4b2a17f --- /dev/null +++ b/modules/lambda_alerting/variables.tf @@ -0,0 +1,17 @@ +variable "aws_region" { type = string } +variable "function_name" { type = string } +variable "handler" { type = string } +variable "runtime" { type = string } +variable "zip_file_path" { type = string } +variable "domain_name" { + type = string + sensitive = true +} +variable "opensearch_endpoint" { + type = string + sensitive = true +} +variable "slack_webhook_url" { + type = string + sensitive = true +} \ No newline at end of file diff --git a/modules/lambda_delivery/delivery.zip b/modules/lambda_delivery/delivery.zip new file mode 100644 index 00000000..0e273d22 Binary files /dev/null and b/modules/lambda_delivery/delivery.zip differ diff --git a/modules/lambda_delivery/index.js b/modules/lambda_delivery/index.js new file mode 100644 index 00000000..13e95cab --- /dev/null +++ b/modules/lambda_delivery/index.js @@ -0,0 +1,88 @@ +const AWS = require("aws-sdk"); +const { Client } = require("@opensearch-project/opensearch"); +const createAwsOpensearchConnector = require("aws-opensearch-connector"); +const zlib = require("zlib"); + +AWS.config.update({ region: process.env.AWS_REGION }); +const s3 = new AWS.S3(); + +const rawEndpoint = process.env.OPENSEARCH_ENDPOINT; +const endpoint = rawEndpoint.startsWith("https://") + ? rawEndpoint + : `https://${rawEndpoint}`; + +const client = new Client({ + ...createAwsOpensearchConnector(AWS.config), + node: endpoint, + ssl: { + rejectUnauthorized: false, + }, +}); + +exports.handler = async (event) => { + const bucket = event.detail?.requestParameters?.bucketName; + const key = event.detail?.requestParameters?.key; + + if (!bucket || !key) { + console.error( + "❌ Missing bucket or key in event:", + JSON.stringify(event, null, 2) + ); + return; + } + + console.log("📥 Received S3 event:", { bucket, key }); + + try { + const obj = await s3.getObject({ Bucket: bucket, Key: key }).promise(); + console.log("📦 Retrieved object from S3 (size):", obj.ContentLength); + + const unzipped = zlib.gunzipSync(obj.Body).toString("utf-8"); + console.log("📂 Unzipped log preview:", unzipped.slice(0, 300)); + + let payload; + try { + payload = JSON.parse(unzipped); + } catch (err) { + console.error("❌ JSON parse error:", err.message); + return; + } + + if (!Array.isArray(payload.Records)) { + console.warn("⚠️ 'Records' is not an array or missing."); + return; + } + + const body = []; + const getIndexName = (log) => { + const date = new Date().toISOString().slice(0, 10); + const source = log.eventSource; + if (source === "wafv2.amazonaws.com") return `waf-logs-${date}`; + return `cloudtrail-logs-${date}`; + }; + + payload.Records.forEach((log) => { + body.push({ index: { _index: getIndexName(log) } }); + + const logWithTimestamp = { + ...log, + "@timestamp": log.eventTime, + }; + + body.push(logWithTimestamp); + }); + + if (body.length > 0) { + const result = await client.bulk({ refresh: true, body }); + if (result?.body?.errors) { + console.error("❌ OpenSearch indexing errors:", result.body.items); + } else { + console.log(`✅ ${body.length / 2} logs indexed to OpenSearch`); + } + } else { + console.log("⚠️ No valid logs to index. Skipping."); + } + } catch (err) { + console.error("❌ Failed to process logs:", err.message || err); + } +}; diff --git a/modules/lambda_delivery/main.tf b/modules/lambda_delivery/main.tf new file mode 100644 index 00000000..5a34d1dc --- /dev/null +++ b/modules/lambda_delivery/main.tf @@ -0,0 +1,79 @@ +data "aws_caller_identity" "current" {} + +resource "aws_iam_role" "lambda_exec" { + name = "${var.function_name}-exec-role" + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [{ + Effect = "Allow" + Principal = { Service = "lambda.amazonaws.com" } + Action = "sts:AssumeRole" + }] + }) +} + +resource "aws_iam_role_policy" "lambda_policy" { + name = "${var.function_name}-policy" + role = aws_iam_role.lambda_exec.id + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Action = ["s3:GetObject"] + Resource = "arn:aws:s3:::${var.bucket_name}/AWSLogs/*" + }, + { + Effect = "Allow" + Action = ["es:ESHttpPost", "es:ESHttpPut", "es:ESHttpGet"] + Resource = "arn:aws:es:${var.aws_region}:${data.aws_caller_identity.current.account_id}:domain/${var.domain_name}/*" + }, + { + Effect = "Allow" + Action = ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"] + Resource = "arn:aws:logs:${var.aws_region}:${data.aws_caller_identity.current.account_id}:log-group:/aws/lambda/${var.function_name}:*" + } + ] + }) +} + +data "aws_kms_alias" "cloudtrail_logs" { + name = var.kms_alias_name +} + +resource "aws_iam_policy" "kms_decrypt" { + name = "AllowKMSDecryptForS3Logs" + + policy = jsonencode({ + Version = "2012-10-17", + Statement : [ + { + Effect = "Allow", + Action = ["kms:Decrypt"], + Resource = data.aws_kms_alias.cloudtrail_logs.target_key_arn + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "attach_kms_policy" { + role = aws_iam_role.lambda_exec.name + policy_arn = aws_iam_policy.kms_decrypt.arn +} +resource "aws_lambda_function" "delivery" { + function_name = var.function_name + handler = var.handler + runtime = var.runtime + role = aws_iam_role.lambda_exec.arn + filename = "${path.module}/${var.zip_file_path}" + source_code_hash = filebase64sha256("${path.module}/${var.zip_file_path}") + + environment { + variables = { + OPENSEARCH_ENDPOINT = var.opensearch_endpoint + } + } +} + +output "lambda_function_name" { value = aws_lambda_function.delivery.function_name } +output "lambda_function_arn" { value = aws_lambda_function.delivery.arn } \ No newline at end of file diff --git a/modules/lambda_delivery/package-lock.json b/modules/lambda_delivery/package-lock.json new file mode 100644 index 00000000..32f5c0c9 --- /dev/null +++ b/modules/lambda_delivery/package-lock.json @@ -0,0 +1,615 @@ +{ + "name": "s3-to-opensearch-delivery", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "s3-to-opensearch-delivery", + "version": "1.0.0", + "dependencies": { + "@opensearch-project/opensearch": "^2.1.0", + "aws-opensearch-connector": "^1.1.0", + "aws-sdk": "^2.1340.0" + } + }, + "node_modules/@opensearch-project/opensearch": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@opensearch-project/opensearch/-/opensearch-2.13.0.tgz", + "integrity": "sha512-Bu3jJ7pKzumbMMeefu7/npAWAvFu5W9SlbBow1ulhluqUpqc7QoXe0KidDrMy7Dy3BQrkI6llR3cWL4lQTZOFw==", + "dependencies": { + "aws4": "^1.11.0", + "debug": "^4.3.1", + "hpagent": "^1.2.0", + "json11": "^2.0.0", + "ms": "^2.1.3", + "secure-json-parse": "^2.4.0" + }, + "engines": { + "node": ">=10", + "yarn": "^1.22.10" + } + }, + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/aws-opensearch-connector": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/aws-opensearch-connector/-/aws-opensearch-connector-1.2.0.tgz", + "integrity": "sha512-Jex72kdNtkpXdBFkA8n/VkWqI+S0AjqY6xGN8I8qA7W2CAjppBEh6PVRER1zLa/Q+/dqxou15uZ5Qz6NFfmqFQ==", + "dependencies": { + "aws4": "^1.11.0" + }, + "engines": { + "node": ">= 10" + }, + "peerDependencies": { + "@opensearch-project/opensearch": ">=1.0.0" + } + }, + "node_modules/aws-sdk": { + "version": "2.1692.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", + "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", + "hasInstallScript": true, + "dependencies": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "util": "^0.12.4", + "uuid": "8.0.0", + "xml2js": "0.6.2" + }, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hpagent": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hpagent/-/hpagent-1.2.0.tgz", + "integrity": "sha512-A91dYTeIB6NoXG+PxTQpCCDDnfHsW9kc06Lvpu1TEe9gnd6ZFeiBoRO9JvzEv6xK7EX97/dUE8g/vBMTqTS3CA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/is-arguments": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", + "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/json11": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/json11/-/json11-2.0.2.tgz", + "integrity": "sha512-HIrd50UPYmP6sqLuLbFVm75g16o0oZrVfxrsY0EEys22klz8mRoWlX9KAEDOSOR9Q34rcxsyC8oDveGrCz5uLQ==", + "bin": { + "json11": "dist/cli.mjs" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/possible-typed-array-names": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", + "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==" + }, + "node_modules/querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", + "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==" + }, + "node_modules/secure-json-parse": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-2.7.0.tgz", + "integrity": "sha512-6aU+Rwsezw7VR8/nyvKTx8QpWH9FrcYiXXlqC4z5d5XQBDRqtbfsRjnwGyqbi3gddNtWHuEk9OANUotL26qKUw==" + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", + "dependencies": { + "punycode": "1.3.2", + "querystring": "0.2.0" + } + }, + "node_modules/util": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", + "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", + "dependencies": { + "inherits": "^2.0.3", + "is-arguments": "^1.0.4", + "is-generator-function": "^1.0.7", + "is-typed-array": "^1.1.3", + "which-typed-array": "^1.1.2" + } + }, + "node_modules/uuid": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", + "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + } + } +} diff --git a/modules/lambda_delivery/package.json b/modules/lambda_delivery/package.json new file mode 100644 index 00000000..f55c7a34 --- /dev/null +++ b/modules/lambda_delivery/package.json @@ -0,0 +1,9 @@ +{ + "name": "s3-to-opensearch-delivery", + "version": "1.0.0", + "dependencies": { + "aws-sdk": "^2.1340.0", + "@opensearch-project/opensearch": "^2.1.0", + "aws-opensearch-connector": "^1.1.0" + } +} diff --git a/modules/lambda_delivery/variables.tf b/modules/lambda_delivery/variables.tf new file mode 100644 index 00000000..3b0cc882 --- /dev/null +++ b/modules/lambda_delivery/variables.tf @@ -0,0 +1,24 @@ +variable "aws_region" { type = string } +variable "bucket_name" { + type = string + sensitive = true +} +variable "domain_name" { + type = string + sensitive = true +} +variable "function_name" { + type = string + sensitive = true +} +variable "handler" { type = string } +variable "runtime" { type = string } +variable "zip_file_path" { type = string } +variable "opensearch_endpoint" { + type = string + sensitive = true +} +variable "kms_alias_name" { + description = "KMS key alias for CloudTrail logs" + type = string +} \ No newline at end of file diff --git a/modules/opensearch_domain/main.tf b/modules/opensearch_domain/main.tf new file mode 100644 index 00000000..557ce005 --- /dev/null +++ b/modules/opensearch_domain/main.tf @@ -0,0 +1,35 @@ +resource "aws_opensearch_domain" "this" { + domain_name = var.domain_name + engine_version = var.engine_version + + cluster_config { + instance_type = var.instance_type + instance_count = var.instance_count + } + + ebs_options { + ebs_enabled = true + volume_size = var.ebs_size + } + + domain_endpoint_options { + enforce_https = true + } + + access_policies = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Effect = "Allow" + Principal = { AWS = "*" } + Action = "es:*" + Resource = "arn:aws:es:${var.aws_region}:${data.aws_caller_identity.current.account_id}:domain/${var.domain_name}/*" + Condition = { + IpAddress = { "aws:SourceIp" = var.allowed_source_ips } + } + } + ] + }) +} + +data "aws_caller_identity" "current" {} \ No newline at end of file diff --git a/modules/opensearch_domain/outputs.tf b/modules/opensearch_domain/outputs.tf new file mode 100644 index 00000000..fa4e6b3d --- /dev/null +++ b/modules/opensearch_domain/outputs.tf @@ -0,0 +1,10 @@ +output "endpoint" { + value = aws_opensearch_domain.this.endpoint + sensitive = true +} + +output "opensearch_domain_id" { + description = "The ID of the OpenSearch domain" + value = aws_opensearch_domain.this.id + sensitive = true +} \ No newline at end of file diff --git a/modules/opensearch_domain/variables.tf b/modules/opensearch_domain/variables.tf new file mode 100644 index 00000000..2675e220 --- /dev/null +++ b/modules/opensearch_domain/variables.tf @@ -0,0 +1,30 @@ +variable "domain_name" { + type = string + sensitive = true +} + +variable "engine_version" { + type = string +} + +variable "instance_type" { + type = string +} + +variable "instance_count" { + type = number +} + +variable "ebs_size" { + type = number +} + +variable "allowed_source_ips" { + type = list(string) + sensitive = true +} + +variable "aws_region" { + description = "Region where the KMS key is created" + type = string +} \ No newline at end of file diff --git a/modules/s3_logs/main.tf b/modules/s3_logs/main.tf new file mode 100644 index 00000000..996326a8 --- /dev/null +++ b/modules/s3_logs/main.tf @@ -0,0 +1,163 @@ +data "aws_caller_identity" "current" {} +data "aws_caller_identity" "prod" {} + +resource "aws_kms_key" "cloudtrail" { + description = "KMS key for encrypting CloudTrail logs in S3" + deletion_window_in_days = 30 + + # 키 정책: 계정 루트 + CloudTrail 서비스 허용 + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # 이 KMS 키를 만든 계정(root)이 모든 작업을 할 수 있도록 + { + Sid = "AllowAccountRootFullAccess" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" + } + Action = "kms:*" + Resource = "*" + }, + + # CloudTrail 서비스가 이 키로 암호화 작업을 할 수 있도록 + { + Sid = "AllowCloudTrailUseOfKey" + Effect = "Allow" + Principal = { + Service = "cloudtrail.amazonaws.com" + } + Action = [ + "kms:GenerateDataKey*", + "kms:Decrypt" + ] + Resource = "*" + } + ] + }) +} + +resource "aws_kms_alias" "cloudtrail" { + name = var.kms_alias_name + target_key_id = aws_kms_key.cloudtrail.key_id +} + +resource "aws_s3_bucket" "logs" { + bucket = var.bucket_name +} + +resource "aws_s3_bucket_versioning" "logs" { + bucket = aws_s3_bucket.logs.id + + versioning_configuration { + status = "Enabled" + } +} + +resource "aws_s3_bucket_server_side_encryption_configuration" "logs" { + bucket = aws_s3_bucket.logs.id + + rule { + apply_server_side_encryption_by_default { + sse_algorithm = "aws:kms" + kms_master_key_id = aws_kms_key.cloudtrail.arn + } + } +} + +resource "aws_s3_bucket_public_access_block" "block" { + bucket = aws_s3_bucket.logs.id + block_public_acls = true + block_public_policy = true + ignore_public_acls = true + restrict_public_buckets = true +} + +resource "aws_s3_bucket_policy" "cloudtrail" { + bucket = aws_s3_bucket.logs.id + + policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + # HTTPS 아닌 요청 모두 거부 + { + Sid = "DenyInsecureTransport" + Effect = "Deny" + Principal = "*" + Action = "s3:*" + Resource = [ + aws_s3_bucket.logs.arn, + "${aws_s3_bucket.logs.arn}/*" + ] + Condition = { + Bool = { "aws:SecureTransport" = "false" } + } + }, + # ACL 확인 허용 + { + Sid = "AllowCloudTrailAclCheck" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.logs.arn + }, + + # 로그 쓰기 + bucket-owner-full-control ACL 조건 + { + Sid = "AllowCloudTrailWrite" + Effect = "Allow" + Principal = { Service = "cloudtrail.amazonaws.com" } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/*" + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + }, + + # prod 계정의 WAF가 로그 쓸 수 있도록 허용 + { + Sid = "AllowProdWAFWrite" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${var.prod_account_id}:root" + } + Action = "s3:PutObject" + Resource = "${aws_s3_bucket.logs.arn}/AWSLogs/${var.prod_account_id}/*" + Condition = { + StringEquals = { + "s3:x-amz-acl" = "bucket-owner-full-control" + } + } + }, + + # prod 계정의 WAF가 ACL 조회 가능하도록 허용 + { + Sid = "AllowProdWAFAclCheck" + Effect = "Allow" + Principal = { + AWS = "arn:aws:iam::${var.prod_account_id}:root" + } + Action = "s3:GetBucketAcl" + Resource = aws_s3_bucket.logs.arn + } + + ] + }) +} + +resource "aws_s3_bucket_lifecycle_configuration" "logs" { + bucket = aws_s3_bucket.logs.id + + rule { + id = "expire-logs-after-30-days" + status = "Enabled" + + filter { prefix = "" } + + expiration { + days = 30 + } + } +} \ No newline at end of file diff --git a/modules/s3_logs/outputs.tf b/modules/s3_logs/outputs.tf new file mode 100644 index 00000000..2781a629 --- /dev/null +++ b/modules/s3_logs/outputs.tf @@ -0,0 +1,11 @@ +output "bucket_name" { + value = aws_s3_bucket.logs.bucket +} + +output "bucket_arn" { + value = aws_s3_bucket.logs.arn +} + +output "kms_key_arn" { + value = aws_kms_key.cloudtrail.arn +} \ No newline at end of file diff --git a/modules/s3_logs/variables.tf b/modules/s3_logs/variables.tf new file mode 100644 index 00000000..2300b642 --- /dev/null +++ b/modules/s3_logs/variables.tf @@ -0,0 +1,21 @@ +variable "bucket_name" { type = string } + +variable "aws_region" { + description = "Region where the KMS key is created" + type = string +} + +variable "management_account_id" { + description = "Account ID of the management account (for S3 bucket policy)" + type = string +} + +variable "kms_alias_name" { + description = "KMS key alias for CloudTrail logs" + type = string +} + +variable "prod_account_id" { + description = "The AWS account ID for the prod account" + type = string +} \ No newline at end of file diff --git a/operation-team-account/monitoring/main.tf b/operation-team-account/monitoring/main.tf new file mode 100644 index 00000000..b5c8fadf --- /dev/null +++ b/operation-team-account/monitoring/main.tf @@ -0,0 +1,92 @@ +terraform { + required_version = ">= 1.1.0" + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + } + backend "s3" { + bucket = "cloudfence-operation-state" + key = "monitoring/terraform.tfstate" + region = "ap-northeast-2" + encrypt = true + dynamodb_table = "tfstate-operation-lock" + } +} + +provider "aws" { + region = var.aws_region +} + +provider "aws" { + alias = "management" + region = var.aws_region +} + +data "aws_caller_identity" "management" { + provider = aws.management +} + +provider "aws" { + alias = "prod" + region = var.aws_region +} + +data "aws_caller_identity" "prod" { + provider = aws.prod +} + +data "aws_caller_identity" "current" {} + +module "s3" { + source = "../../modules/s3_logs" + bucket_name = var.cloudtrail_bucket_name + aws_region = var.aws_region + kms_alias_name = var.kms_alias_name + management_account_id = data.aws_caller_identity.management.account_id + prod_account_id = data.aws_caller_identity.prod.account_id +} + +module "opensearch_domain" { + source = "../../modules/opensearch_domain" + domain_name = var.opensearch_domain_name + engine_version = var.opensearch_engine_version + instance_type = var.opensearch_instance_type + instance_count = var.opensearch_instance_count + ebs_size = var.opensearch_ebs_size + allowed_source_ips = var.allowed_source_ips + aws_region = var.aws_region +} + +module "lambda_alerting" { + source = "../../modules/lambda_alerting" + aws_region = var.aws_region + function_name = "opensearch-alerting-setup" + handler = "index.handler" + runtime = "nodejs22.x" + zip_file_path = "init-alerting.zip" + domain_name = var.opensearch_domain_name + opensearch_endpoint = module.opensearch_domain.endpoint + slack_webhook_url = var.slack_webhook_url +} + +module "lambda_delivery" { + source = "../../modules/lambda_delivery" + aws_region = var.aws_region + domain_name = var.opensearch_domain_name + function_name = "s3-to-opensearch-delivery" + handler = "index.handler" + runtime = "nodejs22.x" + zip_file_path = "delivery.zip" + bucket_name = module.s3.bucket_name + opensearch_endpoint = module.opensearch_domain.endpoint + kms_alias_name = var.kms_alias_name +} + +module "eventbridge" { + source = "../../modules/eventbridge_triggers" + bucket_name = module.s3.bucket_name + lambda_function_name = module.lambda_delivery.lambda_function_name + lambda_function_arn = module.lambda_delivery.lambda_function_arn +} \ No newline at end of file diff --git a/operation-team-account/monitoring/outputs.tf b/operation-team-account/monitoring/outputs.tf new file mode 100644 index 00000000..c2d9999b --- /dev/null +++ b/operation-team-account/monitoring/outputs.tf @@ -0,0 +1,29 @@ +output "opensearch_endpoint" { + description = "Endpoint URL of the OpenSearch domain" + value = module.opensearch_domain.endpoint + sensitive = true +} + +output "bucket_name" { + description = "S3 bucket name for CloudTrail logs" + value = module.s3.bucket_name + sensitive = true +} + +output "bucket_arn" { + description = "S3 bucket ARN for CloudTrail logs" + value = module.s3.bucket_arn + sensitive = true +} + +output "kms_key_arn" { + description = "KMS key ARN used to encrypt CloudTrail logs" + value = module.s3.kms_key_arn + sensitive = true +} + +output "operation_account_id" { + description = "Account ID of the operation account" + value = data.aws_caller_identity.current.account_id + sensitive = true +} \ No newline at end of file diff --git a/operation-team-account/monitoring/variables.tf b/operation-team-account/monitoring/variables.tf new file mode 100644 index 00000000..e9a074dc --- /dev/null +++ b/operation-team-account/monitoring/variables.tf @@ -0,0 +1,63 @@ +variable "aws_region" { + description = "AWS Region" + type = string + default = "ap-northeast-2" +} + +variable "cloudtrail_bucket_name" { + description = "S3 bucket name used by organization CloudTrail" + type = string + default = "whs-aws-logs" + sensitive = true +} + +variable "opensearch_domain_name" { + description = "OpenSearch domain name" + type = string + default = "whs-domain" + sensitive = true +} + +variable "opensearch_engine_version" { + description = "OpenSearch engine version" + type = string + default = "OpenSearch_2.11" +} + +variable "opensearch_instance_type" { + description = "OpenSearch instance type" + type = string + default = "t3.small.search" +} + +variable "opensearch_instance_count" { + description = "Number of OpenSearch instances" + type = number + default = 1 +} + +variable "opensearch_ebs_size" { + description = "EBS volume size for OpenSearch" + type = number + default = 10 +} + +variable "slack_webhook_url" { + description = "Slack Webhook URL" + type = string + sensitive = true +} + +variable "kms_alias_name" { + description = "KMS key alias for CloudTrail logs" + type = string + default = "alias/cloudtrail-logs" + sensitive = true +} + +variable "allowed_source_ips" { + description = "List of IPs allowed to access the OpenSearch domain" + type = list(string) + default = [] + sensitive = true +} \ No newline at end of file