Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions internal/matchers/datadog/datadog.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines 189 to +191

for _, indicator := range indicators {
if strings.Contains(rawSignal, indicator) {
if strings.Contains(rawSignal, strings.ToLower(indicator)) {
Comment on lines 193 to +194
logger.WithField("indicator", indicator).Debug("Found matching signal based on provided indicators")
return true
}
Expand Down
24 changes: 24 additions & 0 deletions internal/matchers/datadog/datadog_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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))
}
4 changes: 2 additions & 2 deletions internal/matchers/elastic/elastic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Comment on lines 368 to +370

for _, indicator := range indicators {
if strings.Contains(alertString, indicator) {
if strings.Contains(alertString, strings.ToLower(indicator)) {
Comment on lines 372 to +373
return true
}
}
Expand Down
20 changes: 20 additions & 0 deletions internal/matchers/elastic/elastic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
36 changes: 34 additions & 2 deletions pack/gcp/gcp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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.
Comment on lines +150 to +154
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.
Expand Down
12 changes: 12 additions & 0 deletions pack/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.<name>, <existing>)`.
Expand Down
6 changes: 4 additions & 2 deletions web/frontend/src/routes/+layout.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -108,8 +108,10 @@
</DropdownMenu.Root>
</div>
</header>
<main class="flex-1 p-6 min-w-0">
{@render children()}
<main class="flex-1 min-w-0">
<div class="mx-auto w-full max-w-[1536px] p-6">
Comment on lines +111 to +112
{@render children()}
</div>
</main>
</SidebarUI.SidebarInset>
</SidebarUI.SidebarProvider>
Expand Down