diff --git a/pkg/analysis/passes/coderules/coderules_test.go b/pkg/analysis/passes/coderules/coderules_test.go index 236ed5f7..b5f9bdb0 100644 --- a/pkg/analysis/passes/coderules/coderules_test.go +++ b/pkg/analysis/passes/coderules/coderules_test.go @@ -317,6 +317,306 @@ func TestOldReactInternals(t *testing.T) { ) } +func TestOutdatedSqldsVersion(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "outdated-sqlds-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 2) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Outdated sqlds version detected (v1 or v2). Use sqlds/v3 or sqlds/v4 which have updated signatures that allow passing context.Context for forward compatibility.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Warning, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-outdated-sqlds-version", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestOutdatedSqldsVersionGood(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "outdated-sqlds-good"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 0) +} + +func TestNativeBrowserDialogs(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "native-browser-dialogs-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 4) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Native browser dialogs (alert, confirm, prompt) are not permitted. Use Grafana UI components (Modal, ConfirmModal) instead.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Error, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-native-browser-dialogs", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestFmtPrintLogging(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "fmt-print-logging"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 3) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Use the logger provided by the Grafana plugin SDK (github.com/grafana/grafana-plugin-sdk-go/backend) instead of fmt.Println/fmt.Print/fmt.Printf for proper log management and integration with Grafana's logging system.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Error, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-fmt-print-logging", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestWindowOpenWithoutNoopener(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "window-open-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 3) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "window.open() called without 'noopener,noreferrer' in the features parameter. This creates a tab nabbing vulnerability. Use window.open(url, target, 'noopener,noreferrer').", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Error, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-window-open-without-noopener", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestWindowOpenWithNoopenerGood(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "window-open-good"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 0) +} + +func TestDeprecatedGfFormCSSClasses(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "deprecated-gf-form-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Greater(t, len(interceptor.Diagnostics), 0) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Deprecated Grafana CSS class name detected (gf-form*). Use @grafana/ui components instead of legacy CSS classes.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Warning, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-deprecated-gf-form-css-classes", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestDirectWindowLocationAccess(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "direct-window-location-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 4) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Direct access to window.location is not permitted. Use locationService from @grafana/runtime instead.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Warning, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-direct-window-location-access", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestDirectWindowLocationAccessGood(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "direct-window-location-good"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 0) +} + +func TestTsIgnoreSuppress(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "ts-ignore-bad"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 2) + for i := range interceptor.Diagnostics { + require.Equal( + t, + "Avoid using @ts-ignore or @ts-expect-error to suppress TypeScript errors. Fix TypeScript errors properly so issues are caught during compilation rather than at runtime.", + interceptor.Diagnostics[i].Title, + ) + require.Equal(t, analysis.Warning, interceptor.Diagnostics[i].Severity) + require.Equal( + t, + "code-rules-ts-ignore-suppress", + interceptor.Diagnostics[i].Name, + ) + } +} + +func TestTsIgnoreSuppressGood(t *testing.T) { + if !isSemgrepInstalled() { + t.Skip("semgrep not installed, skipping test") + return + } + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]any{ + sourcecode.Analyzer: filepath.Join("testdata", "ts-ignore-good"), + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + require.Len(t, interceptor.Diagnostics, 0) +} + func TestNoDirectCSSImports(t *testing.T) { if !isSemgrepInstalled() { t.Skip("semgrep not installed, skipping test") diff --git a/pkg/analysis/passes/coderules/semgrep-rules.yaml b/pkg/analysis/passes/coderules/semgrep-rules.yaml index a5d4f917..3e017c53 100644 --- a/pkg/analysis/passes/coderules/semgrep-rules.yaml +++ b/pkg/analysis/passes/coderules/semgrep-rules.yaml @@ -111,6 +111,14 @@ rules: languages: [go] severity: ERROR + - id: outdated-sqlds-version + pattern-either: + - pattern-regex: '"github\.com/grafana/sqlds"' + - pattern-regex: '"github\.com/grafana/sqlds/v2' + message: "Outdated sqlds version detected (v1 or v2). Use sqlds/v3 or sqlds/v4 which have updated signatures that allow passing context.Context for forward compatibility." + languages: [go] + severity: WARNING + - id: console-logging pattern-either: - pattern: console.log(...) @@ -194,6 +202,114 @@ rules: languages: [javascript, typescript] severity: WARNING + - id: native-browser-dialogs + pattern-either: + - pattern: alert(...) + - pattern: window.alert(...) + - pattern: window.confirm(...) + - pattern: window.prompt(...) + paths: + include: + - "src/**/*.ts" + - "src/**/*.tsx" + exclude: + - "*.spec.ts" + - "*.spec.tsx" + - "*.test.ts" + - "*.test.tsx" + - "*.js" + message: "Native browser dialogs (alert, confirm, prompt) are not permitted. Use Grafana UI components (Modal, ConfirmModal) instead." + languages: [javascript, typescript] + severity: ERROR + + - id: fmt-print-logging + pattern-either: + - pattern: fmt.Println(...) + - pattern: fmt.Print(...) + - pattern: fmt.Printf(...) + message: "Use the logger provided by the Grafana plugin SDK (github.com/grafana/grafana-plugin-sdk-go/backend) instead of fmt.Println/fmt.Print/fmt.Printf for proper log management and integration with Grafana's logging system." + languages: [go] + severity: ERROR + + - id: window-open-without-noopener + pattern-either: + - patterns: + - pattern: window.open($URL) + - patterns: + - pattern: window.open($URL, $TARGET) + - patterns: + - pattern: window.open($URL, $TARGET, $FEATURES) + - metavariable-regex: + metavariable: $FEATURES + regex: ^(?!.*noopener.*noreferrer|.*noreferrer.*noopener) + paths: + include: + - "src/**/*.ts" + - "src/**/*.tsx" + exclude: + - "*.spec.ts" + - "*.spec.tsx" + - "*.test.ts" + - "*.test.tsx" + - "*.js" + message: "window.open() called without 'noopener,noreferrer' in the features parameter. This creates a tab nabbing vulnerability. Use window.open(url, target, 'noopener,noreferrer')." + languages: [javascript, typescript] + severity: ERROR + + - id: deprecated-gf-form-css-classes + pattern-either: + - pattern-regex: gf-form + paths: + include: + - "src/**/*.ts" + - "src/**/*.tsx" + exclude: + - "*.spec.ts" + - "*.spec.tsx" + - "*.test.ts" + - "*.test.tsx" + - "*.js" + message: "Deprecated Grafana CSS class name detected (gf-form*). Use @grafana/ui components instead of legacy CSS classes." + languages: [javascript, typescript] + severity: WARNING + + - id: direct-window-location-access + pattern-either: + - pattern: window.location.search + - pattern: window.location.href + - pattern: new URLSearchParams(window.location.search) + paths: + include: + - "src/**/*.ts" + - "src/**/*.tsx" + exclude: + - "*.spec.ts" + - "*.spec.tsx" + - "*.test.ts" + - "*.test.tsx" + - "*.js" + message: "Direct access to window.location is not permitted. Use locationService from @grafana/runtime instead." + languages: [javascript, typescript] + severity: WARNING + + - id: ts-ignore-suppress + pattern-either: + - pattern-regex: "@ts-ignore" + - pattern-regex: "@ts-expect-error" + paths: + include: + - "src/**/*.ts" + - "src/**/*.tsx" + exclude: + - "*.spec.ts" + - "*.spec.tsx" + - "*.test.ts" + - "*.test.tsx" + - "*.js" + message: "Avoid using @ts-ignore or @ts-expect-error to suppress TypeScript errors. Fix TypeScript errors properly so issues are caught during compilation rather than at runtime." + languages: [typescript] + severity: WARNING + - id: no-direct-css-imports pattern-either: - pattern-regex: import\s+['"].*\.(css|scss|less)['"] diff --git a/pkg/analysis/passes/coderules/testdata/access-env-allowed/main.go b/pkg/analysis/passes/coderules/testdata/access-env-allowed/main.go index 97e3c7f9..67736481 100644 --- a/pkg/analysis/passes/coderules/testdata/access-env-allowed/main.go +++ b/pkg/analysis/passes/coderules/testdata/access-env-allowed/main.go @@ -1,7 +1,6 @@ package donotinclude import ( - "fmt" "os" ) @@ -21,10 +20,10 @@ func DoNotInclude() { // GF_PLUGIN are allowed env := os.Getenv("GF_PLUGIN_ALLOWED_ENV") - fmt.Println(env) + _ = env // NOT allowed for _, e := range os.Environ() { - fmt.Println(e) + _ = e } } diff --git a/pkg/analysis/passes/coderules/testdata/access-env/main.go b/pkg/analysis/passes/coderules/testdata/access-env/main.go index b8c8e366..c4f0e59f 100644 --- a/pkg/analysis/passes/coderules/testdata/access-env/main.go +++ b/pkg/analysis/passes/coderules/testdata/access-env/main.go @@ -9,9 +9,9 @@ func DoNotInclude() { panic("This function should never be included in the binary.") env := os.Getenv("DO_NOT_INCLUDE") - fmt.Println(env) + log.Info(env) for _, e := range os.Environ() { - fmt.Println(e) + log.Info(e) } } diff --git a/pkg/analysis/passes/coderules/testdata/deprecated-gf-form-bad/src/index.ts b/pkg/analysis/passes/coderules/testdata/deprecated-gf-form-bad/src/index.ts new file mode 100644 index 00000000..93c19809 --- /dev/null +++ b/pkg/analysis/passes/coderules/testdata/deprecated-gf-form-bad/src/index.ts @@ -0,0 +1,10 @@ +import React from 'react'; + +export const MyComponent = () => { + return