From 0e5198d225c2a039d63f6d3c3b830007a442b67d Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Wed, 18 Feb 2026 13:03:38 -0500 Subject: [PATCH] feat: add support for stale security alerts monitoring (v0.22.0) --- README.md | 101 ++++++++++++++++++++------------------ examples/complete/main.tf | 32 +++++++++--- main.tf | 42 ++++++++++++++++ outputs.tf | 10 ++++ variables.tf | 49 +++++++++++++++--- 5 files changed, 174 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 29c1aa4..967f0f4 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,17 @@ # terraform-aws-github-ops-app -Terraform module that deploys the [GitHub Ops App](https://github.com/cruxstack/github-ops-app) -to AWS Lambda with API Gateway for webhook handling. +Terraform module that deploys the +[GitHub Ops App](https://github.com/cruxstack/github-ops-app) to AWS Lambda with +API Gateway for webhook handling. ## Features -- **Okta Group Sync** - Automatically syncs Okta groups to GitHub teams based - on configurable rules +- **Okta Group Sync** - Automatically syncs Okta groups to GitHub teams based on + configurable rules - **Orphaned User Detection** - Identifies org members not in any synced Okta teams -- **PR Compliance Monitoring** - Detects when PRs bypass branch protection - rules -- **Automatic Reconciliation** - Detects external team changes and triggers - sync +- **PR Compliance Monitoring** - Detects when PRs bypass branch protection rules +- **Automatic Reconciliation** - Detects external team changes and triggers sync - **Slack Notifications** - Rich messages for violations and sync reports ## Usage @@ -74,7 +73,7 @@ module "github_ops_app" { ## Quick Start -- **Create a GitHub App** with the required permissions +- **Create a GitHub App** with the required permissions - [GitHub Ops App documentation](https://github.com/cruxstack/github-ops-app)) - **Deploy this module** with your GitHub App credentials - **Configure the webhook URL** in your GitHub App settings using the @@ -89,22 +88,22 @@ module "github_ops_app" { ## Inputs -| Name | Description | Type | Default | Required | -|-------------------------------|------------------------------------------------------------------------------|----------------|------------------------------------------------------|:--------:| -| `admin_token_config` | Configuration for admin token protecting some endpoints | `object` | `{ enabled = true }` | no | -| `api_gateway_config` | Configuration for the API Gateway | `object` | `{}` | no | -| `bot_force_rebuild_id` | ID to force rebuilding the Lambda function source code | `string` | `""` | no | -| `bot_repo` | GitHub repository URL for the GitHub Ops App source code | `string` | `"https://github.com/cruxstack/github-ops-app.git"` | no | -| `bot_version` | Version of the GitHub Ops App to use (`latest` or specific tag like `v0.1.0`)| `string` | `"latest"` | no | -| `github_app_config` | GitHub App configuration for authentication and webhook handling | `object` | n/a | yes | -| `lambda_config` | Configuration for the Lambda function | `object` | `{}` | no | -| `lambda_environment_variables`| Additional environment variables for the Lambda function | `map(string)` | `{}` | no | -| `lambda_log_retention_days` | Number of days to retain Lambda function logs | `number` | `30` | no | -| `okta_config` | Okta configuration for user and group synchronization | `object` | `{}` | no | -| `okta_sync_schedule` | EventBridge schedule configuration for automatic Okta sync | `object` | `{}` | no | -| `pr_compliance_config` | Configuration for PR compliance monitoring | `object` | `{}` | no | -| `slack_config` | Slack integration configuration for notifications | `object` | `{}` | no | -| `ssm_parameter_arns` | List of SSM Parameter Store ARNs for secrets retrieval | `list(string)` | `[]` | no | +| Name | Description | Type | Default | Required | +| ------------------------------ | ----------------------------------------------------------------------------- | -------------- | --------------------------------------------------- | :------: | +| `admin_token_config` | Configuration for admin token protecting some endpoints | `object` | `{ enabled = true }` | no | +| `api_gateway_config` | Configuration for the API Gateway | `object` | `{}` | no | +| `bot_force_rebuild_id` | ID to force rebuilding the Lambda function source code | `string` | `""` | no | +| `bot_repo` | GitHub repository URL for the GitHub Ops App source code | `string` | `"https://github.com/cruxstack/github-ops-app.git"` | no | +| `bot_version` | Version of the GitHub Ops App to use (`latest` or specific tag like `v0.1.0`) | `string` | `"latest"` | no | +| `github_app_config` | GitHub App configuration for authentication and webhook handling | `object` | n/a | yes | +| `lambda_config` | Configuration for the Lambda function | `object` | `{}` | no | +| `lambda_environment_variables` | Additional environment variables for the Lambda function | `map(string)` | `{}` | no | +| `lambda_log_retention_days` | Number of days to retain Lambda function logs | `number` | `30` | no | +| `okta_config` | Okta configuration for user and group synchronization | `object` | `{}` | no | +| `okta_sync_schedule` | EventBridge schedule configuration for automatic Okta sync | `object` | `{}` | no | +| `pr_compliance_config` | Configuration for PR compliance monitoring | `object` | `{}` | no | +| `slack_config` | Slack integration configuration for notifications | `object` | `{}` | no | +| `ssm_parameter_arns` | List of SSM Parameter Store ARNs for secrets retrieval | `list(string)` | `[]` | no | ### GitHub App Config @@ -161,7 +160,9 @@ sync_rules = [ ] ``` -See the [GitHub Ops App documentation](https://github.com/cruxstack/github-ops-app/blob/main/docs/okta-setup.md#step-10-configure-sync-rules) for detailed sync rule configuration. +See the +[GitHub Ops App documentation](https://github.com/cruxstack/github-ops-app/blob/main/docs/okta-setup.md#step-10-configure-sync-rules) +for detailed sync rule configuration. ### Lambda Config @@ -188,7 +189,8 @@ slack_config = { } ``` -Per-notification channels are optional and fall back to the default `channel` if not specified. +Per-notification channels are optional and fall back to the default `channel` if +not specified. ### Admin Token Config @@ -199,29 +201,32 @@ admin_token_config = { } ``` -When enabled, requests to `/server/*` and `/scheduled/*` endpoints require an `Authorization: Bearer ` header. If no token is provided, a secure 32-character token is automatically generated and available via the `admin_token` output. +When enabled, requests to `/server/*` and `/scheduled/*` endpoints require an +`Authorization: Bearer ` header. If no token is provided, a secure +32-character token is automatically generated and available via the +`admin_token` output. ## Outputs -| Name | Description | -|----------------------------------|------------------------------------------------------| -| `lambda_function_arn` | ARN of the GitHub Ops App Lambda function | -| `lambda_function_name` | Name of the GitHub Ops App Lambda function | -| `lambda_function_qualified_arn` | Qualified ARN of the Lambda function | -| `lambda_function_invoke_arn` | Invoke ARN of the Lambda function | -| `lambda_role_arn` | ARN of the IAM role used by the Lambda function | -| `lambda_role_name` | Name of the IAM role used by the Lambda function | -| `cloudwatch_log_group_name` | Name of the CloudWatch Log Group | -| `cloudwatch_log_group_arn` | ARN of the CloudWatch Log Group | -| `api_gateway_id` | ID of the API Gateway HTTP API | -| `api_gateway_arn` | ARN of the API Gateway HTTP API | -| `api_gateway_endpoint` | Base URL of the API Gateway | -| `api_gateway_execution_arn` | Execution ARN of the API Gateway | -| `webhook_url` | Full webhook URL to configure in GitHub App settings | -| `webhook_secret` | Webhook secret to configure in GitHub App | -| `admin_token` | Admin token for `/server/*` and `/scheduled/*` endpoints | -| `eventbridge_rule_arn` | ARN of the EventBridge rule for scheduled Okta sync | -| `eventbridge_rule_name` | Name of the EventBridge rule | +| Name | Description | +| ------------------------------- | -------------------------------------------------------- | +| `lambda_function_arn` | ARN of the GitHub Ops App Lambda function | +| `lambda_function_name` | Name of the GitHub Ops App Lambda function | +| `lambda_function_qualified_arn` | Qualified ARN of the Lambda function | +| `lambda_function_invoke_arn` | Invoke ARN of the Lambda function | +| `lambda_role_arn` | ARN of the IAM role used by the Lambda function | +| `lambda_role_name` | Name of the IAM role used by the Lambda function | +| `cloudwatch_log_group_name` | Name of the CloudWatch Log Group | +| `cloudwatch_log_group_arn` | ARN of the CloudWatch Log Group | +| `api_gateway_id` | ID of the API Gateway HTTP API | +| `api_gateway_arn` | ARN of the API Gateway HTTP API | +| `api_gateway_endpoint` | Base URL of the API Gateway | +| `api_gateway_execution_arn` | Execution ARN of the API Gateway | +| `webhook_url` | Full webhook URL to configure in GitHub App settings | +| `webhook_secret` | Webhook secret to configure in GitHub App | +| `admin_token` | Admin token for `/server/*` and `/scheduled/*` endpoints | +| `eventbridge_rule_arn` | ARN of the EventBridge rule for scheduled Okta sync | +| `eventbridge_rule_name` | Name of the EventBridge rule | ## Architecture @@ -259,7 +264,7 @@ When enabled, requests to `/server/*` and `/scheduled/*` endpoints require an `A ## Requirements | Name | Version | -|-----------|---------| +| --------- | ------- | | terraform | >= 1.3 | | aws | >= 5.0 | diff --git a/examples/complete/main.tf b/examples/complete/main.tf index bc078ac..4273289 100644 --- a/examples/complete/main.tf +++ b/examples/complete/main.tf @@ -69,14 +69,28 @@ module "github_ops_app" { monitored_branches = ["main", "master", "release/*"] } + # security alerts monitoring + security_alerts_config = { + enabled = true + min_age_days = 30 # only report alerts older than 30 days + min_severity = "high" # minimum severity: critical, high, medium, low + } + + # schedule security alerts check daily + security_alerts_schedule = { + enabled = true + schedule_expression = "rate(24 hours)" + } + # slack notifications (optional) slack_config = { - enabled = true - token = var.slack_token - channel = var.slack_channel - channel_pr_bypass = var.slack_channel_pr_bypass # optional: override for PR bypass alerts - channel_okta_sync = var.slack_channel_okta_sync # optional: override for sync reports - channel_orphaned_users = var.slack_channel_orphaned_users # optional: override for orphaned user alerts + enabled = true + token = var.slack_token + channel = var.slack_channel + channel_pr_bypass = var.slack_channel_pr_bypass # optional: override for PR bypass alerts + channel_okta_sync = var.slack_channel_okta_sync # optional: override for sync reports + channel_orphaned_users = var.slack_channel_orphaned_users # optional: override for orphaned user alerts + channel_security_alerts = var.slack_channel_security_alerts # optional: override for security alerts } # lambda configuration @@ -184,6 +198,12 @@ variable "slack_channel_orphaned_users" { default = "" } +variable "slack_channel_security_alerts" { + type = string + description = "Slack channel ID for security alerts (optional, falls back to slack_channel)" + default = "" +} + # ----------------------------------------------------------------------------- # outputs # ----------------------------------------------------------------------------- diff --git a/main.tf b/main.tf index 5c18f92..c9ac134 100644 --- a/main.tf +++ b/main.tf @@ -28,7 +28,14 @@ locals { # pr compliance config APP_PR_COMPLIANCE_ENABLED = tostring(var.pr_compliance_config.enabled) APP_PR_MONITORED_BRANCHES = var.pr_compliance_config.enabled ? join(",", var.pr_compliance_config.monitored_branches) : "" + + # security alerts config + APP_SECURITY_ALERTS_ENABLED = tostring(var.security_alerts_config.enabled) }, + var.security_alerts_config.enabled ? { + APP_SECURITY_ALERTS_MIN_AGE_DAYS = tostring(var.security_alerts_config.min_age_days) + APP_SECURITY_ALERTS_MIN_SEVERITY = var.security_alerts_config.min_severity + } : {}, # admin token config (conditional) local.admin_token != "" ? { APP_ADMIN_TOKEN = local.admin_token } : {}, # okta config (conditional) @@ -48,6 +55,7 @@ locals { }, var.slack_config.channel_okta_sync != "" ? { APP_SLACK_CHANNEL_OKTA_SYNC = var.slack_config.channel_okta_sync } : {}, var.slack_config.channel_orphaned_users != "" ? { APP_SLACK_CHANNEL_ORPHANED_USERS = var.slack_config.channel_orphaned_users } : {}, + var.slack_config.channel_security_alerts != "" ? { APP_SLACK_CHANNEL_SECURITY_ALERTS = var.slack_config.channel_security_alerts } : {}, var.slack_config.channel_pr_bypass != "" ? { APP_SLACK_CHANNEL_PR_BYPASS = var.slack_config.channel_pr_bypass APP_SLACK_FOOTER_NOTE_PR_BYPASS = var.pr_compliance_config.slack_footer_note @@ -251,6 +259,40 @@ resource "aws_lambda_permission" "eventbridge" { source_arn = aws_cloudwatch_event_rule.okta_sync[0].arn } +resource "aws_cloudwatch_event_rule" "security_alerts" { + count = local.enabled && var.security_alerts_schedule.enabled ? 1 : 0 + + name = "${module.this.id}-security-alerts" + description = "Scheduled trigger for GitHub security alerts monitoring" + schedule_expression = var.security_alerts_schedule.schedule_expression + tags = module.this.tags +} + +resource "aws_cloudwatch_event_target" "security_alerts" { + count = local.enabled && var.security_alerts_schedule.enabled ? 1 : 0 + + rule = aws_cloudwatch_event_rule.security_alerts[0].name + target_id = "SecurityAlertsLambda" + arn = aws_lambda_function.this[0].arn + + input = jsonencode({ + path = "/scheduled/security-alerts" + httpMethod = "POST" + headers = {} + body = "" + }) +} + +resource "aws_lambda_permission" "eventbridge_security_alerts" { + count = local.enabled && var.security_alerts_schedule.enabled ? 1 : 0 + + statement_id = "AllowExecutionFromEventBridgeSecurityAlerts" + action = "lambda:InvokeFunction" + function_name = aws_lambda_function.this[0].function_name + principal = "events.amazonaws.com" + source_arn = aws_cloudwatch_event_rule.security_alerts[0].arn +} + # ---------------------------------------------------------------------- iam --- resource "aws_iam_role" "this" { diff --git a/outputs.tf b/outputs.tf index ff7f856..24c72b5 100644 --- a/outputs.tf +++ b/outputs.tf @@ -94,3 +94,13 @@ output "eventbridge_rule_name" { description = "Name of the EventBridge rule for scheduled Okta sync (if enabled)" value = try(aws_cloudwatch_event_rule.okta_sync[0].name, null) } + +output "eventbridge_security_alerts_rule_arn" { + description = "ARN of the EventBridge rule for scheduled security alerts monitoring (if enabled)" + value = try(aws_cloudwatch_event_rule.security_alerts[0].arn, null) +} + +output "eventbridge_security_alerts_rule_name" { + description = "Name of the EventBridge rule for scheduled security alerts monitoring (if enabled)" + value = try(aws_cloudwatch_event_rule.security_alerts[0].name, null) +} diff --git a/variables.tf b/variables.tf index 66b36a6..8216d25 100644 --- a/variables.tf +++ b/variables.tf @@ -136,6 +136,42 @@ variable "okta_sync_schedule" { } } +# ------------------------------------------------------- security alerts --- + +variable "security_alerts_config" { + description = "Configuration for monitoring stale GitHub security alerts (Dependabot, code scanning, and secret scanning)." + type = object({ + enabled = optional(bool, false) + min_age_days = optional(number, 30) + min_severity = optional(string, "high") + }) + default = {} + + validation { + condition = var.security_alerts_config.min_age_days >= 1 + error_message = "Security alerts min_age_days must be at least 1." + } + + validation { + condition = contains(["critical", "high", "medium", "low"], var.security_alerts_config.min_severity) + error_message = "Security alerts min_severity must be one of: critical, high, medium, low." + } +} + +variable "security_alerts_schedule" { + description = "EventBridge schedule configuration for periodic security alerts monitoring." + type = object({ + enabled = optional(bool, false) + schedule_expression = optional(string, "rate(24 hours)") + }) + default = {} + + validation { + condition = !var.security_alerts_schedule.enabled || can(regex("^(rate|cron)\\(", var.security_alerts_schedule.schedule_expression)) + error_message = "Schedule expression must be a valid EventBridge rate or cron expression." + } +} + # ----------------------------------------------------------- pr compliance --- variable "pr_compliance_config" { @@ -154,12 +190,13 @@ variable "slack_config" { description = "Slack integration configuration for sending notifications." sensitive = true type = object({ - enabled = optional(bool, false) - token = optional(string, "") - channel = optional(string, "") - channel_pr_bypass = optional(string, "") - channel_okta_sync = optional(string, "") - channel_orphaned_users = optional(string, "") + enabled = optional(bool, false) + token = optional(string, "") + channel = optional(string, "") + channel_pr_bypass = optional(string, "") + channel_okta_sync = optional(string, "") + channel_orphaned_users = optional(string, "") + channel_security_alerts = optional(string, "") }) default = {}