diff --git a/semgrep-scan/rules/go.yml b/semgrep-scan/rules/go.yml index 92ef708..698af54 100644 --- a/semgrep-scan/rules/go.yml +++ b/semgrep-scan/rules/go.yml @@ -204,16 +204,27 @@ rules: - pattern-either: - pattern: rds.NewInstance(...) - pattern: rds.NewCluster(...) - # Suppress only when `StorageEncrypted` is explicitly set to a - # truthy value. Two forms are accepted: + # Suppress when `StorageEncrypted` is set in a way that is NOT a + # static-disable AND is secure-by-default. Three accepted shapes: # * `StorageEncrypted: pulumi.Bool(true)` / `sdk.Bool(true)` / - # anything that ends `.Bool(true)` (the canonical Pulumi shape) - # * bare `StorageEncrypted: true` (less idiomatic but valid) - # Important: setting the field to `false` (or `pulumi.Bool(false)`, - # or any non-true value) does NOT suppress — we want to flag those - # explicitly-disabled cases, not let them through. + # any `.Bool(true)` — the canonical Pulumi truthy shape. + # * bare `StorageEncrypted: true`. + # * `StorageEncrypted: .Bool(lo.FromPtrOr(<*bool>, true))` + # — runtime-config with secure default. The literal `true` + # fallback is required: `lo.FromPtr(*ptr)` (nil → false → DB + # unencrypted-by-default) and `lo.FromPtrOr(*ptr, false)` + # both still fire. Pair this shape with + # `IgnoreChanges([]string{"storageEncrypted"})` on the + # resource opts so the default flip doesn't propose a + # destructive replacement on existing unencrypted instances + # (the pairing isn't verified in regex; the wrapper shape + + # literal-true fallback are the design signal). + # Setting the field to `false` (literal), `Bool(false)`, or + # `lo.FromPtr(*bool)` (= `lo.FromPtrOr(*bool, false)`) still + # fires — those default to unencrypted. - pattern-not-regex: 'StorageEncrypted:\s*\w+\.Bool\(true\)' - pattern-not-regex: 'StorageEncrypted:\s*true\b' + - pattern-not-regex: 'StorageEncrypted:\s*\w+\.Bool\(lo\.FromPtrOr\([^()\n]+,\s*true\s*\)\)' paths: include: - "**/*.go" diff --git a/semgrep-scan/tests/go_cases.go b/semgrep-scan/tests/go_cases.go index 3c9337d..4361268 100644 --- a/semgrep-scan/tests/go_cases.go +++ b/semgrep-scan/tests/go_cases.go @@ -219,6 +219,82 @@ func rdsExplicitlyDisabledBool(ctx *pulumiCtx) { }) } +// Runtime-config secure-by-default shape: +// `.Bool(lo.FromPtrOr(<*bool>, true))`. Used in +// simple-container-com/api: nil → true (encrypted), explicit value +// otherwise. Paired with `IgnoreChanges([]string{"storageEncrypted"})` +// on the resource opts (not shown — pairing isn't regex-verified). +// Must NOT fire. +type pulumiSdk struct{} + +func (pulumiSdk) Bool(_ bool) bool { return false } + +var sdk = pulumiSdk{} +var pulumi = pulumiSdk{} + +type loPkg struct{} + +func (loPkg) FromPtr(_ *bool) bool { return false } +func (loPkg) FromPtrOr(_ *bool, _ bool) bool { return false } + +var lo = loPkg{} + +type dbConfigShape struct{ StorageEncrypted *bool } + +func rdsRuntimeSecureSdk(ctx *pulumiCtx) { + cfg := dbConfigShape{} + // ok: go-aws-rds-no-storage-encryption + _ = rds.NewInstance(ctx, "db4", &instanceArgs{ + Engine: "postgres", + StorageEncrypted: sdk.Bool(lo.FromPtrOr(cfg.StorageEncrypted, true)), + }) +} + +// Same shape with `pulumi.Bool` prefix — must also NOT fire. +func rdsRuntimeSecurePulumi(ctx *pulumiCtx) { + cfg := dbConfigShape{} + // ok: go-aws-rds-no-storage-encryption + _ = rds.NewCluster(ctx, "cluster3", &clusterArgs{ + Engine: "aurora-postgresql", + StorageEncrypted: pulumi.Bool(lo.FromPtrOr(cfg.StorageEncrypted, true)), + }) +} + +// Bare `lo.FromPtr(*ptr)` defaults to false when nil — DB ends up +// unencrypted-by-default. Must STILL FIRE. +func rdsBareFromPtrStillFires(ctx *pulumiCtx) { + cfg := dbConfigShape{} + // ruleid: go-aws-rds-no-storage-encryption + _ = rds.NewInstance(ctx, "db5", &instanceArgs{ + Engine: "postgres", + StorageEncrypted: sdk.Bool(lo.FromPtr(cfg.StorageEncrypted)), + }) +} + +// `lo.FromPtrOr(*ptr, false)` is just bare-FromPtr in disguise — the +// fallback is unencrypted, so the DB defaults to unencrypted. Must +// STILL FIRE. +func rdsFromPtrOrFalseStillFires(ctx *pulumiCtx) { + cfg := dbConfigShape{} + // ruleid: go-aws-rds-no-storage-encryption + _ = rds.NewInstance(ctx, "db6", &instanceArgs{ + Engine: "postgres", + StorageEncrypted: sdk.Bool(lo.FromPtrOr(cfg.StorageEncrypted, false)), + }) +} + +// Arbitrary wrapper that isn't the recognized helper — rule must +// FIRE (we don't trust unknown runtime expressions). +func rdsArbitraryWrapperStillFires(ctx *pulumiCtx) { + // ruleid: go-aws-rds-no-storage-encryption + _ = rds.NewInstance(ctx, "db7", &instanceArgs{ + Engine: "postgres", + StorageEncrypted: sdk.Bool(someRuntimeBool()), + }) +} + +func someRuntimeBool() bool { return false } + // -------------------------------------------------------------------- // go-fmt-errorf-percent-v-for-error // --------------------------------------------------------------------