From 4510708f23e9ec54dd7a005c38c8382f6a803d44 Mon Sep 17 00:00:00 2001 From: Google Cloud Agent for Compute Workloads Dev Date: Mon, 27 Jul 2026 02:42:23 -0700 Subject: [PATCH] [WLM for MySQL] Add rule for checking public access exposure PiperOrigin-RevId: 954509511 --- internal/mysqlmetrics/mysqlmetrics.go | 85 +++++++++++++++------- internal/mysqlmetrics/mysqlmetrics_test.go | 75 +++++++++++++++---- 2 files changed, 117 insertions(+), 43 deletions(-) diff --git a/internal/mysqlmetrics/mysqlmetrics.go b/internal/mysqlmetrics/mysqlmetrics.go index 1b7831d..f3a167a 100644 --- a/internal/mysqlmetrics/mysqlmetrics.go +++ b/internal/mysqlmetrics/mysqlmetrics.go @@ -39,15 +39,21 @@ import ( ) const ( - innoDBKey = "is_inno_db_default" - bufferPoolKey = "buffer_pool_size" - totalRAMKey = "total_ram" - currentRoleKey = "current_role" - replicationZonesKey = "replication_zones" - notProtectedByAutoFailoverKey = "not_protected_by_auto_failover" - sourceRole = "source" - replicaRole = "replica" - replicationZonesQuery = "SELECT HOST FROM information_schema.PROCESSLIST AS p WHERE p.COMMAND = 'Binlog Dump'" + innoDBKey = "is_inno_db_default" + bufferPoolKey = "buffer_pool_size" + totalRAMKey = "total_ram" + currentRoleKey = "current_role" + replicationZonesKey = "replication_zones" + notProtectedByAutoFailoverKey = "not_protected_by_auto_failover" + noAutomatedBackupPolicyKey = "no_automated_backup_policy" + lastBackupOldKey = "last_backup_old" + rootPasswordNotSetKey = "root_password_not_set" + exposedToPublicAccessKey = "exposed_to_public_access" + unencryptedConnectionsAllowedKey = "unencrypted_connections_allowed" + auditingEnabledKey = "auditing_enabled" + sourceRole = "source" + replicaRole = "replica" + replicationZonesQuery = "SELECT HOST FROM information_schema.PROCESSLIST AS p WHERE p.COMMAND = 'Binlog Dump'" ) type netInterface interface { @@ -1135,29 +1141,54 @@ func (m *MySQLMetrics) CollectWlmMetricsOnce(ctx context.Context, dwActivated bo } currentRole := m.currentRole(ctx) replicationZones := m.replicationZones(ctx, currentRole, &netImpl{}) - notProtectedByAutoFailover, err := m.notProtectedByAutoFailover(ctx) - if err != nil { - log.CtxLogger(ctx).Warnf("Failed to get not protected by auto failover: %v", err) - } - log.CtxLogger(ctx).Debugw("Finished collecting MySQL metrics once. Next step is to send to WLM (DW).", - bufferPoolKey, bufferPoolSize, - totalRAMKey, totalRAM, - innoDBKey, isInnoDBDefault, - currentRoleKey, currentRole, - replicationZonesKey, strings.Join(replicationZones, ","), - notProtectedByAutoFailoverKey, notProtectedByAutoFailover, - ) metrics := workloadmanager.WorkloadMetrics{ WorkloadType: workloadmanager.MYSQL, Metrics: map[string]string{ - bufferPoolKey: strconv.FormatInt(bufferPoolSize, 10), - totalRAMKey: strconv.Itoa(totalRAM), - innoDBKey: strconv.FormatBool(isInnoDBDefault), - currentRoleKey: currentRole, - replicationZonesKey: strings.Join(replicationZones, ","), - notProtectedByAutoFailoverKey: strconv.FormatBool(notProtectedByAutoFailover), + bufferPoolKey: strconv.FormatInt(bufferPoolSize, 10), + totalRAMKey: strconv.Itoa(totalRAM), + innoDBKey: strconv.FormatBool(isInnoDBDefault), + currentRoleKey: currentRole, + replicationZonesKey: strings.Join(replicationZones, ","), }, } + // To ensure the metric collection loop remains resilient as new rules are added, these evaluation metrics do not early-return on error; + // we simply won't populate them in the map. If collection fails, we log a warning and omit the key so that it defaults to ""NULL" in DWH. + if notProtectedByAutoFailover, err := m.notProtectedByAutoFailover(ctx); err == nil { + metrics.Metrics[notProtectedByAutoFailoverKey] = strconv.FormatBool(notProtectedByAutoFailover) + } else { + log.CtxLogger(ctx).Warnf("Failed to get not protected by auto failover: %v", err) + } + if noAutomatedBackupPolicy, err := m.noAutomatedBackupPolicy(ctx); err == nil { + metrics.Metrics[noAutomatedBackupPolicyKey] = strconv.FormatBool(noAutomatedBackupPolicy) + } else { + log.CtxLogger(ctx).Warnf("Failed to get no automated backup policy: %v", err) + } + if lastBackupOld, err := m.lastBackupOld(ctx); err == nil { + metrics.Metrics[lastBackupOldKey] = strconv.FormatBool(lastBackupOld) + } else { + log.CtxLogger(ctx).Warnf("Failed to get last backup old: %v", err) + } + if rootPasswordNotSet, err := m.rootPasswordNotSet(ctx); err == nil { + metrics.Metrics[rootPasswordNotSetKey] = strconv.FormatBool(rootPasswordNotSet) + } else { + log.CtxLogger(ctx).Warnf("Failed to get root password not set: %v", err) + } + if exposedToPublicAccess, err := m.exposedToPublicAccess(ctx); err == nil { + metrics.Metrics[exposedToPublicAccessKey] = strconv.FormatBool(exposedToPublicAccess) + } else { + log.CtxLogger(ctx).Warnf("Failed to get exposed to public access: %v", err) + } + if unencryptedConnectionsAllowed, err := m.unencryptedConnectionsAllowed(ctx); err == nil { + metrics.Metrics[unencryptedConnectionsAllowedKey] = strconv.FormatBool(unencryptedConnectionsAllowed) + } else { + log.CtxLogger(ctx).Warnf("Failed to get unencrypted connections allowed: %v", err) + } + if auditingEnabled, err := m.auditingEnabled(ctx); err == nil { + metrics.Metrics[auditingEnabledKey] = strconv.FormatBool(auditingEnabled) + } else { + log.CtxLogger(ctx).Warnf("Failed to get auditing enabled: %v", err) + } + log.CtxLogger(ctx).Debugw("Finished collecting MySQL metrics once. Next step is to send to WLM (DW).", "metrics", metrics.Metrics) res, err := workloadmanager.SendDataInsight(ctx, workloadmanager.SendDataInsightParams{ WLMetrics: metrics, CloudProps: m.Config.GetCloudProperties(), diff --git a/internal/mysqlmetrics/mysqlmetrics_test.go b/internal/mysqlmetrics/mysqlmetrics_test.go index b32441f..e79795e 100644 --- a/internal/mysqlmetrics/mysqlmetrics_test.go +++ b/internal/mysqlmetrics/mysqlmetrics_test.go @@ -1218,8 +1218,19 @@ func TestCollectWlmMetricsOnce(t *testing.T) { shouldErr: false, }, engineErr: nil, - bufferPoolRows: &bufferPoolRows{count: 0, size: 1, data: 134217728, shouldErr: false}, - bufferPoolErr: nil, + bufferPoolRows: &bufferPoolRows{count: 0, size: 1, data: 134217728, shouldErr: false}, + bufferPoolErr: nil, + mebHistoryRows: &countMockRows{size: 1, data: 0}, + xtrabackupHistoryRows: &countMockRows{size: 1, data: 0}, + mysqlUserRows: &mysqlUserMockRows{ + size: 1, + data: [][]any{ + {"root", "localhost", "mysql_native_password", sql.NullString{String: "hash", Valid: true}}, + }, + }, + exposedToPublicAccessRows: &exposedToPublicAccessMockRows{size: 0}, + requireSecureTransportRows: &globalVarMockRows{size: 1, data: [][]string{{"require_secure_transport", "ON"}}}, + auditLogPluginRows: &pluginStatusMockRows{size: 0}, }, execute: func(context.Context, commandlineexecutor.Params) commandlineexecutor.Result { return commandlineexecutor.Result{ @@ -1237,12 +1248,18 @@ func TestCollectWlmMetricsOnce(t *testing.T) { wantMetrics: &workloadmanager.WorkloadMetrics{ WorkloadType: workloadmanager.MYSQL, Metrics: map[string]string{ - bufferPoolKey: "134217728", - currentRoleKey: sourceRole, - totalRAMKey: strconv.Itoa(4025040 * 1024), - innoDBKey: "true", - replicationZonesKey: "", - notProtectedByAutoFailoverKey: "true", + bufferPoolKey: "134217728", + currentRoleKey: sourceRole, + totalRAMKey: strconv.Itoa(4025040 * 1024), + innoDBKey: "true", + replicationZonesKey: "", + notProtectedByAutoFailoverKey: "true", + noAutomatedBackupPolicyKey: "true", + lastBackupOldKey: "true", + rootPasswordNotSetKey: "false", + exposedToPublicAccessKey: "false", + unencryptedConnectionsAllowedKey: "false", + auditingEnabledKey: "false", }, }, wantErr: false, @@ -1390,6 +1407,15 @@ func TestCollectWlmMetricsOnce(t *testing.T) { engineErr: nil, bufferPoolRows: &bufferPoolRows{count: 0, size: 1, data: 134217728, shouldErr: false}, bufferPoolErr: nil, + mysqlUserRows: &mysqlUserMockRows{ + size: 1, + data: [][]any{ + {"root", "localhost", "mysql_native_password", sql.NullString{String: "hash", Valid: true}}, + }, + }, + exposedToPublicAccessRows: &exposedToPublicAccessMockRows{size: 0}, + requireSecureTransportRows: &globalVarMockRows{size: 1, data: [][]string{{"require_secure_transport", "ON"}}}, + auditLogPluginRows: &pluginStatusMockRows{size: 0}, }, execute: func(context.Context, commandlineexecutor.Params) commandlineexecutor.Result { return commandlineexecutor.Result{ @@ -1434,8 +1460,19 @@ func TestCollectWlmMetricsOnce(t *testing.T) { shouldErr: false, }, engineErr: nil, - bufferPoolRows: &bufferPoolRows{count: 0, size: 1, data: 134217728, shouldErr: false}, - bufferPoolErr: nil, + bufferPoolRows: &bufferPoolRows{count: 0, size: 1, data: 134217728, shouldErr: false}, + bufferPoolErr: nil, + mebHistoryRows: &countMockRows{size: 1, data: 0}, + xtrabackupHistoryRows: &countMockRows{size: 1, data: 0}, + mysqlUserRows: &mysqlUserMockRows{ + size: 1, + data: [][]any{ + {"root", "localhost", "mysql_native_password", sql.NullString{String: "hash", Valid: true}}, + }, + }, + exposedToPublicAccessRows: &exposedToPublicAccessMockRows{size: 0}, + requireSecureTransportRows: &globalVarMockRows{size: 1, data: [][]string{{"require_secure_transport", "ON"}}}, + auditLogPluginRows: &pluginStatusMockRows{size: 0}, }, execute: func(context.Context, commandlineexecutor.Params) commandlineexecutor.Result { return commandlineexecutor.Result{ @@ -1451,12 +1488,18 @@ func TestCollectWlmMetricsOnce(t *testing.T) { wantMetrics: &workloadmanager.WorkloadMetrics{ WorkloadType: workloadmanager.MYSQL, Metrics: map[string]string{ - bufferPoolKey: "134217728", - currentRoleKey: sourceRole, - totalRAMKey: strconv.Itoa(4025040 * 1024), - innoDBKey: "true", - replicationZonesKey: "", - notProtectedByAutoFailoverKey: "true", + bufferPoolKey: "134217728", + currentRoleKey: sourceRole, + totalRAMKey: strconv.Itoa(4025040 * 1024), + innoDBKey: "true", + replicationZonesKey: "", + notProtectedByAutoFailoverKey: "true", + noAutomatedBackupPolicyKey: "true", + lastBackupOldKey: "true", + rootPasswordNotSetKey: "false", + exposedToPublicAccessKey: "false", + unencryptedConnectionsAllowedKey: "false", + auditingEnabledKey: "false", }, }, wantErr: false,