From e72cb39a6f61aaee756af5e4ebf2c065950e76f2 Mon Sep 17 00:00:00 2001 From: Pavel Lineitsev Date: Thu, 18 Jun 2026 15:30:36 -0400 Subject: [PATCH 1/4] fix: adjust layout padding for improved UI scaling --- web/frontend/src/routes/+layout.svelte | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/web/frontend/src/routes/+layout.svelte b/web/frontend/src/routes/+layout.svelte index 77b4c6d..d5fe9bf 100644 --- a/web/frontend/src/routes/+layout.svelte +++ b/web/frontend/src/routes/+layout.svelte @@ -108,8 +108,10 @@ -
- {@render children()} +
+
+ {@render children()} +
From 5fd7a48ec3f48b6ac1f683213d8ee44decc56a9a Mon Sep 17 00:00:00 2001 From: Pavel Lineitsev Date: Thu, 18 Jun 2026 15:31:05 -0400 Subject: [PATCH 2/4] fix: implement case-insensitive matching for signals and alerts --- internal/matchers/datadog/datadog.go | 4 ++-- internal/matchers/datadog/datadog_test.go | 24 +++++++++++++++++++++++ internal/matchers/elastic/elastic.go | 4 ++-- internal/matchers/elastic/elastic_test.go | 20 +++++++++++++++++++ 4 files changed, 48 insertions(+), 4 deletions(-) diff --git a/internal/matchers/datadog/datadog.go b/internal/matchers/datadog/datadog.go index 0e85e77..27b3cf3 100644 --- a/internal/matchers/datadog/datadog.go +++ b/internal/matchers/datadog/datadog.go @@ -188,10 +188,10 @@ func (m *DatadogAlertGeneratedAssertion) findMatchingSignal(signals []datadogV2. func (m *DatadogAlertGeneratedAssertion) signalMatchesExecution(signal datadogV2.SecurityMonitoringSignal, indicators []string, logger *logrus.Entry) bool { buf, _ := json.Marshal(signal.Attributes.Custom) - rawSignal := string(buf) + rawSignal := strings.ToLower(string(buf)) for _, indicator := range indicators { - if strings.Contains(rawSignal, indicator) { + if strings.Contains(rawSignal, strings.ToLower(indicator)) { logger.WithField("indicator", indicator).Debug("Found matching signal based on provided indicators") return true } diff --git a/internal/matchers/datadog/datadog_test.go b/internal/matchers/datadog/datadog_test.go index a914dda..82c2a07 100644 --- a/internal/matchers/datadog/datadog_test.go +++ b/internal/matchers/datadog/datadog_test.go @@ -8,6 +8,7 @@ import ( "github.com/DataDog/datadog-api-client-go/v2/api/datadogV2" "github.com/IBM/simrun/internal/matchers/datadog/mocks" "github.com/aws/smithy-go/ptr" + "github.com/sirupsen/logrus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" @@ -184,3 +185,26 @@ func TestDatadog(t *testing.T) { } } + +// TestSignalMatchesExecutionCaseInsensitive guards against providers (e.g. Azure) +// emitting custom fields in a different case than the indicator. Matching must +// succeed regardless of casing on either side; this test fails on the old +// case-sensitive strings.Contains implementation. +func TestSignalMatchesExecutionCaseInsensitive(t *testing.T) { + testLogger := logrus.WithFields(logrus.Fields{"matcher": "test"}) + + signal := datadogV2.NewSecurityMonitoringSignal() + signal.Id = ptr.String("1") + signal.Attributes = &datadogV2.SecurityMonitoringSignalAttributes{Custom: map[string]interface{}{ + "resource_id": "/SUBSCRIPTIONS/ABC-123/RESOURCEGROUPS/RG", + }} + + matcher := DatadogAlertGeneratedAssertion{} + + // Indicator is lower-case, signal value is upper-case. + assert.True(t, matcher.signalMatchesExecution(*signal, []string{"abc-123"}, testLogger)) + // Indicator is differently cased from the signal value. + assert.True(t, matcher.signalMatchesExecution(*signal, []string{"/subscriptions/abc-123/resourcegroups/rg"}, testLogger)) + // Genuinely absent value still does not match. + assert.False(t, matcher.signalMatchesExecution(*signal, []string{"def-456"}, testLogger)) +} diff --git a/internal/matchers/elastic/elastic.go b/internal/matchers/elastic/elastic.go index b127f10..ccb10d7 100644 --- a/internal/matchers/elastic/elastic.go +++ b/internal/matchers/elastic/elastic.go @@ -367,10 +367,10 @@ func CloseAlerts(api ElasticSecurityDetectionAlertsAPI, alertIDs []string, logge func alertMatchesIndicators(alert ElasticSecurityDetectionAlert, indicators []string) bool { alertBytes, _ := json.Marshal(alert.Source) - alertString := string(alertBytes) + alertString := strings.ToLower(string(alertBytes)) for _, indicator := range indicators { - if strings.Contains(alertString, indicator) { + if strings.Contains(alertString, strings.ToLower(indicator)) { return true } } diff --git a/internal/matchers/elastic/elastic_test.go b/internal/matchers/elastic/elastic_test.go index 1596b53..56c7d73 100644 --- a/internal/matchers/elastic/elastic_test.go +++ b/internal/matchers/elastic/elastic_test.go @@ -34,6 +34,26 @@ func TestElasticAlertMatchesExecution(t *testing.T) { assert.False(t, matches) } +// TestAlertMatchesIndicatorsCaseInsensitive guards against providers (e.g. Azure) +// emitting fields in a different case than the indicator. Matching must succeed +// regardless of casing on either side; this test fails on the old case-sensitive +// strings.Contains implementation. +func TestAlertMatchesIndicatorsCaseInsensitive(t *testing.T) { + alert := ElasticSecurityDetectionAlert{ + ID: "test-alert-1", + Source: map[string]interface{}{ + "azure.resource_id": "/SUBSCRIPTIONS/ABC-123/RESOURCEGROUPS/RG", + }, + } + + // Indicator is lower-case, alert value is upper-case. + assert.True(t, alertMatchesIndicators(alert, []string{"abc-123"})) + // Indicator is upper-case, alert value is upper-case but differently cased. + assert.True(t, alertMatchesIndicators(alert, []string{"/subscriptions/abc-123/resourcegroups/rg"})) + // Genuinely absent value still does not match. + assert.False(t, alertMatchesIndicators(alert, []string{"def-456"})) +} + func TestBuildElasticAlertQuery(t *testing.T) { // Test the query building method assertion := &ElasticSecurityAlertGeneratedAssertion{ From bb397ee8248b380dfc8c8ed4351950eeab094b3a Mon Sep 17 00:00:00 2001 From: Pavel Lineitsev Date: Thu, 18 Jun 2026 15:32:04 -0400 Subject: [PATCH 3/4] fix: add storage-related resources to no-tags mapping --- pack/tags.go | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pack/tags.go b/pack/tags.go index 8da1fc0..006abb6 100644 --- a/pack/tags.go +++ b/pack/tags.go @@ -292,6 +292,18 @@ var azureNoTagsResources = map[string]bool{ "azurerm_role_assignment": true, "azurerm_role_definition": true, "azurerm_management_lock": true, + // Storage data-plane resources are scoped to a storage account and do + // not accept tags. + "azurerm_storage_container": true, + "azurerm_storage_blob": true, + "azurerm_storage_share": true, + "azurerm_storage_share_directory": true, + "azurerm_storage_share_file": true, + "azurerm_storage_queue": true, + "azurerm_storage_table": true, + "azurerm_storage_table_entity": true, + "azurerm_storage_data_lake_gen2_filesystem": true, + "azurerm_storage_data_lake_gen2_path": true, } // mergeVarTokens builds tokens for `merge(var., )`. From 4c2eabc4d44dfe66c71e2db914a09028f980f1f7 Mon Sep 17 00:00:00 2001 From: Pavel Lineitsev Date: Thu, 18 Jun 2026 15:32:28 -0400 Subject: [PATCH 4/4] fix: enhance GCP credentials handling to support multiple credential types --- pack/gcp/gcp.go | 36 ++++++++++++++++++++++++++++++++++-- 1 file changed, 34 insertions(+), 2 deletions(-) diff --git a/pack/gcp/gcp.go b/pack/gcp/gcp.go index fde9744..2cd739c 100644 --- a/pack/gcp/gcp.go +++ b/pack/gcp/gcp.go @@ -123,10 +123,16 @@ func ClientOptions(ctx context.Context) ([]option.ClientOption, error) { opts = append(opts, option.WithUserAgent(pack.UserAgent(executionID))) } - // Check for inline credentials JSON + // Check for inline credentials JSON. GOOGLE_CREDENTIALS may hold either a + // service_account key (legacy) or an external_account config (Workload + // Identity Federation), so detect the type rather than forcing one. if credsJSON := os.Getenv("GOOGLE_CREDENTIALS"); credsJSON != "" { + credType, err := credentialsType(credsJSON) + if err != nil { + return nil, fmt.Errorf("detect GCP credential type: %w", err) + } creds, err := google.CredentialsFromJSONWithTypeAndParams(ctx, []byte(credsJSON), - google.ServiceAccount, + credType, google.CredentialsParams{Scopes: []string{"https://www.googleapis.com/auth/cloud-platform"}}, ) if err != nil { @@ -141,6 +147,32 @@ func ClientOptions(ctx context.Context) ([]option.ClientOption, error) { return opts, nil } +// credentialsType inspects the "type" field of a GCP credentials JSON document +// and returns the matching CredentialsType. It supports service_account keys, +// external_account (Workload Identity Federation) configs, and authorized_user +// credentials. The credentials are produced by the simrun credential resolver, +// not an untrusted source. +func credentialsType(credsJSON string) (google.CredentialsType, error) { + var doc struct { + Type string `json:"type"` + } + if err := json.Unmarshal([]byte(credsJSON), &doc); err != nil { + return "", fmt.Errorf("invalid GCP credentials JSON: %w", err) + } + switch doc.Type { + case string(google.ServiceAccount): + return google.ServiceAccount, nil + case string(google.ExternalAccount): + return google.ExternalAccount, nil + case string(google.AuthorizedUser): + return google.AuthorizedUser, nil + case "": + return "", fmt.Errorf("missing credential type in GCP credentials JSON") + default: + return "", fmt.Errorf("unsupported GCP credential type %q", doc.Type) + } +} + // ImpersonateServiceAccount returns a client option that uses impersonated credentials // for the specified service account. This allows simulations to test privilege escalation // scenarios by attempting operations with limited permissions.