From d2fb85238a43e2ddb723e50df615f0aef06cec02 Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:04:14 -0500 Subject: [PATCH 1/2] fix: build shv2 console urls --- README.md | 13 +++++++------ internal/app/app.go | 2 +- internal/app/config.go | 26 ++++++++++++++------------ internal/events/events.go | 2 +- internal/events/finding.go | 31 ++++++++++++++++++++++++------- 5 files changed, 47 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 7e3e616..1324f2b 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,13 @@ cd dist && zip deployment.zip bootstrap && cd .. ## Optional Environment Variables -| name | example | purpose | default | -| --------------------------- | ------------------------------------------ | ------------------------------------------------------------ | --------------------------------- | -| `APP_DEBUG_ENABLED` | `true` | verbose logging & event dump | `false` | -| `APP_AWS_CONSOLE_URL` | `https://console.aws.amazon.com` | base AWS console URL | `https://console.aws.amazon.com` | -| `APP_AWS_ACCESS_PORTAL_URL` | `https://myorg.awsapps.com/start` | AWS access portal URL (for federated access) | _(none - direct console links)_ | -| `APP_AWS_ACCESS_ROLE_NAME` | `SecurityAuditor` | IAM role name for access portal | _(none - direct console links)_ | +| name | example | purpose | default | +| ------------------------------ | ------------------------------------------ | ------------------------------------------------------------ | --------------------------------- | +| `APP_DEBUG_ENABLED` | `true` | verbose logging & event dump | `false` | +| `APP_AWS_CONSOLE_URL` | `https://console.aws.amazon.com` | base AWS console URL | `https://console.aws.amazon.com` | +| `APP_AWS_ACCESS_PORTAL_URL` | `https://myorg.awsapps.com/start` | AWS access portal URL (for federated access) | _(none - direct console links)_ | +| `APP_AWS_ACCESS_ROLE_NAME` | `SecurityAuditor` | IAM role name for access portal | _(none - direct console links)_ | +| `APP_AWS_SECURITYHUBV2_REGION` | `us-east-1` | AWS region for centralized SecurityHub v2 if applicable | _(none - direct console links)_ | ## Create Lambda Function diff --git a/internal/app/app.go b/internal/app/app.go index e94aee4..fe733c2 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -46,7 +46,7 @@ func (a *App) Process(evt awsEvent.CloudWatchEvent) error { if err != nil || !e.IsAlertable() { return err } - m0, m1 := e.SlackMessage(a.Config.AwsConsoleURL, a.Config.AwsAccessPortalURL, a.Config.AwsAccessRoleName) + m0, m1 := e.SlackMessage(a.Config.AwsConsoleURL, a.Config.AwsAccessPortalURL, a.Config.AwsAccessRoleName, a.Config.AWSSecurityHubv2Region) _, _, err = a.SlackClient.PostMessage(a.Config.SlackChannel, m0, m1) return err } diff --git a/internal/app/config.go b/internal/app/config.go index 1bd714e..02b2f25 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -8,24 +8,26 @@ import ( ) type Config struct { - DebugEnabled bool - AwsConsoleURL string - AwsAccessPortalURL string - AwsAccessRoleName string - SlackToken string - SlackChannel string + DebugEnabled bool + AwsConsoleURL string + AwsAccessPortalURL string + AwsAccessRoleName string + AWSSecurityHubv2Region string + SlackToken string + SlackChannel string } func NewConfig() (*Config, error) { debugEnabled, _ := strconv.ParseBool(os.Getenv("APP_DEBUG_ENABLED")) cfg := Config{ - DebugEnabled: debugEnabled, - AwsConsoleURL: os.Getenv("APP_AWS_CONSOLE_URL"), - AwsAccessPortalURL: os.Getenv("APP_AWS_ACCESS_PORTAL_URL"), - AwsAccessRoleName: os.Getenv("APP_AWS_ACCESS_ROLE_NAME"), - SlackToken: os.Getenv("APP_SLACK_TOKEN"), - SlackChannel: os.Getenv("APP_SLACK_CHANNEL"), + DebugEnabled: debugEnabled, + AwsConsoleURL: os.Getenv("APP_AWS_CONSOLE_URL"), + AwsAccessPortalURL: os.Getenv("APP_AWS_ACCESS_PORTAL_URL"), + AwsAccessRoleName: os.Getenv("APP_AWS_ACCESS_ROLE_NAME"), + AWSSecurityHubv2Region: os.Getenv("APP_AWS_SECURITYHUBV2_REGION"), + SlackToken: os.Getenv("APP_SLACK_TOKEN"), + SlackChannel: os.Getenv("APP_SLACK_CHANNEL"), } if cfg.AwsConsoleURL == "" { diff --git a/internal/events/events.go b/internal/events/events.go index 4f1728f..4b6e8dd 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -6,5 +6,5 @@ import ( type SecurityHubEvent interface { IsAlertable() bool - SlackMessage(consoleURL, accessPortalURL, accessRoleName string) (slack.MsgOption, slack.MsgOption) + SlackMessage(consoleURL, accessPortalURL, accessRoleName, shRegion string) (slack.MsgOption, slack.MsgOption) } diff --git a/internal/events/finding.go b/internal/events/finding.go index 03f840f..605064f 100644 --- a/internal/events/finding.go +++ b/internal/events/finding.go @@ -135,7 +135,7 @@ type ResourceTag struct { Value string `json:"value"` } -func (shf *SecurityHubV2Finding) SlackMessage(consoleURL, accessPortalURL, accessRoleName string) (slack.MsgOption, slack.MsgOption) { +func (shf *SecurityHubV2Finding) SlackMessage(consoleURL, accessPortalURL, accessRoleName, shRegion string) (slack.MsgOption, slack.MsgOption) { var blocks []slack.Block severityEmoji := shf.GetSeverityEmoji() @@ -192,7 +192,7 @@ func (shf *SecurityHubV2Finding) SlackMessage(consoleURL, accessPortalURL, acces blocks = append(blocks, remediationSection) } - consoleUrl := shf.BuildConsoleUrl(consoleURL, accessPortalURL, accessRoleName) + consoleUrl := shf.BuildConsoleUrl(consoleURL, accessPortalURL, accessRoleName, shRegion) buttonSection := slack.NewActionBlock( "actions", slack.NewButtonBlockElement( @@ -270,13 +270,30 @@ func (shf *SecurityHubV2Finding) GetSeverityEmoji() string { } } -func (shf *SecurityHubV2Finding) BuildConsoleUrl(consoleURL, accessPortalURL, accessRoleName string) string { - encodedId := url.QueryEscape(shf.FindingInfo.UID) - region := shf.Cloud.Region +func (shf *SecurityHubV2Finding) BuildConsoleUrl(consoleURL, accessPortalURL, accessRoleName, shRegion string) string { + region := shRegion + if region == "" { + region = shf.Cloud.Region + } + + var view string + findingType := shf.GetFindingCategory() + + switch findingType { + case "Exposure": + view = "exposure" + case "Posture Management": + view = "postureManagement" + case "Threats": + view = "threats" + case "Vulnerabilities": + view = "vulnerabilities" + } + // https://883776786067-fwrss4sa.us-east-1.console.aws.amazon.com/securityhub/v2/home?region=us-east-1#/postureManagement?findingDetailId=b864b75ebfd1bf2a9c0353af5a446dd521ca0af231d56d671311494ecdcedbb8&detailPanelTabId=Resources dst := fmt.Sprintf( - "%s/securityhub/home?region=%s#/findings?search=Id%%3D%%255Coperator%%255C%%253AEQUALS%%255C%%253A%s", - consoleURL, region, encodedId, + "%s/securityhub/v2/home?region=%s#/%s?findingDetailId=%s", + consoleURL, region, view, shf.Metadata.UID, ) if accessPortalURL != "" && accessRoleName != "" { From 204ed312271c602b44f28ab0f2a1ad2e9a81155d Mon Sep 17 00:00:00 2001 From: Brian Ojeda <9335829+sgtoj@users.noreply.github.com> Date: Wed, 19 Nov 2025 13:20:34 -0500 Subject: [PATCH 2/2] feat: include finding id in slack message --- internal/events/finding.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/internal/events/finding.go b/internal/events/finding.go index 605064f..ecc5c5f 100644 --- a/internal/events/finding.go +++ b/internal/events/finding.go @@ -161,6 +161,12 @@ func (shf *SecurityHubV2Finding) SlackMessage(consoleURL, accessPortalURL, acces details := slack.NewSectionBlock(nil, detailFields, nil) blocks = append(blocks, details) + findingIDSection := slack.NewSectionBlock( + slack.NewTextBlockObject("mrkdwn", fmt.Sprintf("*Finding ID*\n`%s`", shf.Metadata.UID), false, false), + nil, nil, + ) + blocks = append(blocks, findingIDSection) + if len(shf.Resources) > 0 { resource := shf.Resources[0] var resourceFields []*slack.TextBlockObject