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
2 changes: 2 additions & 0 deletions recipes/activate.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ func Activate(r *recipe.Registry) {
r.Register(&style.RemoveEmptyGoroutine{}, golang, codeQuality, styleCategory)
r.Register(&style.AvoidEmptyInterfaceParam{}, golang, codeQuality, styleCategory)
r.Register(&style.AuditExecCommand{}, golang, codeQuality, styleCategory)
r.Register(&style.AuditBindAllInterfaces{}, golang, codeQuality, styleCategory)
r.Register(&style.AuditGoroutineClosure{}, golang, codeQuality, styleCategory)
r.Register(&style.AvoidHardcodedCredentials{}, golang, codeQuality, styleCategory)
r.Register(&style.UseCustomHttpClient{}, golang, codeQuality, styleCategory)
Expand Down Expand Up @@ -179,6 +180,7 @@ func Activate(r *recipe.Registry) {
r.Register(&errorhandling.UseErrorsIsOverStringComparison{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.UseErrorsAs{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.AvoidLogFatal{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.AuditErrorStringFormat{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.AuditMultipleErrorWraps{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.AvoidOsExit{}, golang, codeQuality, errCategory)
r.Register(&errorhandling.AuditRecover{}, golang, codeQuality, errCategory)
Expand Down
139 changes: 139 additions & 0 deletions recipes/errorhandling/audit_error_string_format.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
/*
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

package errorhandling

import (
"strconv"
"strings"
"unicode"

"github.com/moderneinc/recipes-go/diagnostic"
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/golang"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/java"
"github.com/openrewrite/rewrite/rewrite-go/pkg/visitor"
)

// ErrorStringFormatRow is one row of the AuditErrorStringFormat data table.
type ErrorStringFormatRow struct {
SourcePath string
Constructor string
Issue string
Message string
}

var errorStringFormatTable = recipe.NewDataTable[ErrorStringFormatRow](
"org.openrewrite.golang.codequality.AuditErrorStringFormat$Findings",
"Error strings that violate ST1005",
"Error messages that are capitalized or end with punctuation.",
[]recipe.ColumnDescriptor{
{Name: "sourcePath", DisplayName: "Source path", Description: "File containing the error string.", Type: "String"},
{Name: "constructor", DisplayName: "Constructor", Description: "errors.New or fmt.Errorf.", Type: "String"},
{Name: "issue", DisplayName: "Issue", Description: "Why the message violates ST1005.", Type: "String"},
{Name: "message", DisplayName: "Message", Description: "The offending error message.", Type: "String"},
},
)

// AuditErrorStringFormat finds `errors.New` and `fmt.Errorf` messages that are
// capitalized or end with punctuation. Error strings are often wrapped by a
// caller (`fmt.Errorf("doing x: %w", err)`), so a leading capital or trailing
// period reads badly mid-sentence. This mirrors staticcheck ST1005.
type AuditErrorStringFormat struct {
recipe.Base
}

func (r *AuditErrorStringFormat) Name() string {
return "org.openrewrite.golang.codequality.AuditErrorStringFormat"
}
func (r *AuditErrorStringFormat) DisplayName() string { return "Audit error string format" }
func (r *AuditErrorStringFormat) Description() string {
return "Find `errors.New` and `fmt.Errorf` messages that are capitalized or end with punctuation. Error strings are often embedded in a larger message, so they should not be capitalized or end with punctuation (staticcheck ST1005)."
}
func (r *AuditErrorStringFormat) Tags() []string { return []string{"error-handling", "lint"} }

func (r *AuditErrorStringFormat) DiagnosticMappings() []diagnostic.Mapping {
return []diagnostic.Mapping{
{DiagnosticID: "ST1005", Tool: diagnostic.Staticcheck, HasFix: false},
}
}

func (r *AuditErrorStringFormat) DataTables() []recipe.DataTableDescriptor {
return []recipe.DataTableDescriptor{errorStringFormatTable.Descriptor()}
}

func (r *AuditErrorStringFormat) Editor() recipe.TreeVisitor {
return visitor.Init(&auditErrorStringFormatVisitor{})
}

type auditErrorStringFormatVisitor struct {
visitor.GoVisitor
sourcePath string
}

func (v *auditErrorStringFormatVisitor) VisitCompilationUnit(cu *golang.CompilationUnit, p any) java.J {
v.sourcePath = cu.SourcePath
return v.GoVisitor.VisitCompilationUnit(cu, p)
}

func (v *auditErrorStringFormatVisitor) VisitMethodInvocation(mi *java.MethodInvocation, p any) java.J {
mi = v.GoVisitor.VisitMethodInvocation(mi, p).(*java.MethodInvocation)

if mi.Select == nil {
return mi
}
ident, ok := mi.Select.Element.(*java.Identifier)
if !ok {
return mi
}
pkg, fn := ident.Name, mi.Name.Name
if !((pkg == "errors" && fn == "New") || (pkg == "fmt" && fn == "Errorf")) {
return mi
}

args := mi.Arguments.Elements
if len(args) == 0 {
return mi
}
lit, ok := args[0].Element.(*java.Literal)
if !ok {
return mi
}
msg, err := strconv.Unquote(lit.Source)
if err != nil || msg == "" {
return mi
}

reason := errorStringIssue(msg)
if reason == "" {
return mi
}
if ctx, ok := p.(*recipe.ExecutionContext); ok {
errorStringFormatTable.InsertRow(ctx, ErrorStringFormatRow{
SourcePath: v.sourcePath,
Constructor: pkg + "." + fn,
Issue: reason,
Message: msg,
})
}
return mi.WithMarkers(java.MarkupInfo(mi.Markers, "error string should not "+reason+" (ST1005)"))
}

// errorStringIssue reports why an error message violates ST1005, or "" if it is
// fine. Capitalization is only flagged when the first word looks like an
// ordinary word (leading upper followed by lower), so initialisms and proper
// nouns like "HTTP" or "TLS" are left alone.
func errorStringIssue(msg string) string {
runes := []rune(msg)
if len(runes) >= 2 && unicode.IsUpper(runes[0]) && unicode.IsLower(runes[1]) {
return "be capitalized"
}
if !strings.HasSuffix(msg, "...") {
switch runes[len(runes)-1] {
case '.', ':', '!':
return "end with punctuation"
}
}
return ""
}
134 changes: 134 additions & 0 deletions recipes/style/audit_bind_all_interfaces.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

package style

import (
"net"
"strconv"

"github.com/moderneinc/recipes-go/diagnostic"
"github.com/openrewrite/rewrite/rewrite-go/pkg/recipe"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/golang"
"github.com/openrewrite/rewrite/rewrite-go/pkg/tree/java"
"github.com/openrewrite/rewrite/rewrite-go/pkg/visitor"
)

// BindAllInterfacesRow is one row of the AuditBindAllInterfaces data table.
type BindAllInterfacesRow struct {
SourcePath string
Call string
Address string
}

var bindAllInterfacesTable = recipe.NewDataTable[BindAllInterfacesRow](
"org.openrewrite.golang.codequality.AuditBindAllInterfaces$Findings",
"Listeners bound to all interfaces",
"Network listeners bound to all interfaces.",
[]recipe.ColumnDescriptor{
{Name: "sourcePath", DisplayName: "Source path", Description: "File containing the listener.", Type: "String"},
{Name: "call", DisplayName: "Call", Description: "The listen function invoked.", Type: "String"},
{Name: "address", DisplayName: "Address", Description: "The bound address.", Type: "String"},
},
)

// AuditBindAllInterfaces finds network listeners bound to all interfaces
// (an empty host such as `:8080`, or `0.0.0.0`/`[::]`). Binding to all
// interfaces exposes the service beyond the loopback address and may be
// unintended; prefer an explicit host like `127.0.0.1`. This mirrors gosec
// G102.
type AuditBindAllInterfaces struct {
recipe.Base
}

func (r *AuditBindAllInterfaces) Name() string {
return "org.openrewrite.golang.codequality.AuditBindAllInterfaces"
}
func (r *AuditBindAllInterfaces) DisplayName() string { return "Audit binding to all interfaces" }
func (r *AuditBindAllInterfaces) Description() string {
return "Find network listeners bound to all interfaces (e.g. `:8080`, `0.0.0.0`, `[::]`). This exposes the service beyond loopback and may be unintended; prefer an explicit host such as `127.0.0.1` (gosec G102)."
}
func (r *AuditBindAllInterfaces) Tags() []string { return []string{"style", "security"} }

func (r *AuditBindAllInterfaces) DiagnosticMappings() []diagnostic.Mapping {
return []diagnostic.Mapping{
{DiagnosticID: "G102", Tool: diagnostic.GolangciLint, HasFix: false},
}
}

func (r *AuditBindAllInterfaces) DataTables() []recipe.DataTableDescriptor {
return []recipe.DataTableDescriptor{bindAllInterfacesTable.Descriptor()}
}

func (r *AuditBindAllInterfaces) Editor() recipe.TreeVisitor {
return visitor.Init(&auditBindAllInterfacesVisitor{})
}

type auditBindAllInterfacesVisitor struct {
visitor.GoVisitor
sourcePath string
}

func (v *auditBindAllInterfacesVisitor) VisitCompilationUnit(cu *golang.CompilationUnit, p any) java.J {
v.sourcePath = cu.SourcePath
return v.GoVisitor.VisitCompilationUnit(cu, p)
}

func (v *auditBindAllInterfacesVisitor) VisitMethodInvocation(mi *java.MethodInvocation, p any) java.J {
mi = v.GoVisitor.VisitMethodInvocation(mi, p).(*java.MethodInvocation)

if mi.Select == nil {
return mi
}
ident, ok := mi.Select.Element.(*java.Identifier)
if !ok || !isListenCall(ident.Name, mi.Name.Name) {
return mi
}

for _, arg := range mi.Arguments.Elements {
lit, ok := arg.Element.(*java.Literal)
if !ok {
continue
}
addr, err := strconv.Unquote(lit.Source)
if err != nil {
continue
}
if bindsAllInterfaces(addr) {
if ctx, ok := p.(*recipe.ExecutionContext); ok {
bindAllInterfacesTable.InsertRow(ctx, BindAllInterfacesRow{
SourcePath: v.sourcePath,
Call: ident.Name + "." + mi.Name.Name,
Address: addr,
})
}
return mi.WithMarkers(java.MarkupWarn(mi.Markers, "listener bound to all interfaces; prefer an explicit host such as 127.0.0.1"))
}
}
return mi
}

func isListenCall(pkg, fn string) bool {
switch pkg {
case "net":
return fn == "Listen" || fn == "ListenPacket"
case "tls":
return fn == "Listen"
case "http":
return fn == "ListenAndServe" || fn == "ListenAndServeTLS"
}
return false
}

func bindsAllInterfaces(addr string) bool {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return false
}
switch host {
case "", "0.0.0.0", "::":
return true
}
return false
}
88 changes: 88 additions & 0 deletions tests/errorhandling/audit_error_string_format_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
/*
* Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract.
*/

package errorhandling_test

import (
"testing"

"github.com/moderneinc/recipes-go/recipes/errorhandling"
"github.com/openrewrite/rewrite/rewrite-go/pkg/test"
)

func TestAuditErrorStringFormatCapitalized(t *testing.T) {
spec := test.NewRecipeSpec().WithRecipe(&errorhandling.AuditErrorStringFormat{})
spec.RewriteRun(t,
test.Golang(`
package main

import "errors"

func f() error {
return errors.New("Failed to connect")
}
`, `
package main

import "errors"

func f() error {
return /*~~(error string should not be capitalized (ST1005))~~>*/errors.New("Failed to connect")
}
`),
)
}

func TestAuditErrorStringFormatTrailingPunctuation(t *testing.T) {
spec := test.NewRecipeSpec().WithRecipe(&errorhandling.AuditErrorStringFormat{})
spec.RewriteRun(t,
test.Golang(`
package main

import "fmt"

func f() error {
return fmt.Errorf("connection failed.")
}
`, `
package main

import "fmt"

func f() error {
return /*~~(error string should not end with punctuation (ST1005))~~>*/fmt.Errorf("connection failed.")
}
`),
)
}

func TestAuditErrorStringFormatCleanNoChange(t *testing.T) {
spec := test.NewRecipeSpec().WithRecipe(&errorhandling.AuditErrorStringFormat{})
spec.RewriteRun(t,
test.Golang(`
package main

import "errors"

func f() error {
return errors.New("failed to connect")
}
`),
)
}

func TestAuditErrorStringFormatInitialismNoChange(t *testing.T) {
spec := test.NewRecipeSpec().WithRecipe(&errorhandling.AuditErrorStringFormat{})
spec.RewriteRun(t,
test.Golang(`
package main

import "errors"

func f() error {
return errors.New("TLS handshake failed")
}
`),
)
}
Loading
Loading