Skip to content
Draft
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
22 changes: 22 additions & 0 deletions analysis/directory.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package analysis

import "fmt"

type FunctionDirectory struct {
// Different languages can have the same type of Analysis function.
// This first maps the Type of function-mode, then the specific language implementation
Pool map[string]map[Language]*AnalysisFunction
}

var AnalysisFuncDirectory = &FunctionDirectory{
Pool: make(map[string]map[Language]*AnalysisFunction),
}

func (fd *FunctionDirectory) AddToDirectory(ana *Analyzer) error {
anaFunc := fd.Pool[ana.Name]
if anaFunc == nil {
return fmt.Errorf("%s method is not supported", ana.Name)
}
anaFunc[ana.Language].Analyzer = ana
return nil
}
38 changes: 38 additions & 0 deletions analysis/directory_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package analysis

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestDirectoryPopulation(t *testing.T) {
checker := `name: "run_taint_analysis"
language: javascript
category: security
severity: high
message: "This is just a mock checker"
analysisFunction:
name: taint
parameters:
sources:
- |
(call_expression
function: (identifier) @sourceName
(#eq? @sourceName "getUserInput"))
sinks:
- |
(call_expression
function: (identifier) @sinkName
(#eq? @sinkName "perform_db_operation"))
pattern: |
(call_expression)
description: "Runs a taint analysis on the provided function and its parameters."`

_, _, err := ReadFromBytes([]byte(checker))
assert.NoError(t, err)
assert.NotNil(t, AnalysisFuncDirectory.Pool["taint"])
assert.NotNil(t, AnalysisFuncDirectory.Pool["taint"][LangJs])
fun := AnalysisFuncDirectory.Pool["taint"][LangJs]
assert.Equal(t, fun.Name, "taint")
}
85 changes: 52 additions & 33 deletions analysis/testrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,17 +285,25 @@ type YamlTestCase struct {
TestFile string
}

func RunYamlTests(testDir string) (passed bool, err error) {
type YamlIssues struct {
YamlAnalyzer YamlAnalyzer
Got []int
Want []int
}

var IssuesYaml = make(map[YamlTestCase]*YamlIssues)

func RunYamlTests(testDir string) (issues map[YamlTestCase]*YamlIssues, err error) {
tests, err := FindYamlTestFiles(testDir)
if err != nil {
return false, err
return nil, err
}

if len(tests) == 0 {
return false, fmt.Errorf("no test files found")
return nil, fmt.Errorf("no test files found")
}

passed = true
// passed = true
for _, test := range tests {
if test.TestFile == "" {
fmt.Fprintf(os.Stderr, "No test file found for checker '%s'\n", test.YamlCheckerPath)
Expand All @@ -304,19 +312,19 @@ func RunYamlTests(testDir string) (passed bool, err error) {

fmt.Fprintf(os.Stderr, "Running test case: %s\n", filepath.Base(test.YamlCheckerPath))

checker, _, err := ReadFromFile(test.YamlCheckerPath)
checker, yamlChecker, err := ReadFromFile(test.YamlCheckerPath)
if err != nil {
return false, err
return nil, err
}

want, err := findExpectedLines(test.TestFile)
if err != nil {
return false, err
return nil, err
}

gotIssues, err := RunAnalyzers(test.TestFile, []*Analyzer{&checker}, nil)
if err != nil {
return false, err
return nil, err
}

var got []int
Expand All @@ -326,35 +334,46 @@ func RunYamlTests(testDir string) (passed bool, err error) {

slices.Sort(got)

if len(want) != len(got) {
testName := filepath.Base(test.YamlCheckerPath)
message := fmt.Sprintf(
"(%s): expected issues on the following lines: %v\nbut issues were raised on lines: %v\n",
testName,
want,
got,
)
fmt.Fprintf(os.Stderr, "%s", message)
passed = false
continue
}
for j := 0; j < len(want); j++ {
if want[j] != got[j] {
testName := filepath.Base(test.YamlCheckerPath)
message := fmt.Sprintf(
"(%s): expected issue on line %d, but next occurrence is on line %d\n",
testName,
want[j],
got[j],
)
fmt.Fprintf(os.Stderr, "%s\n", message)
passed = false
if IssuesYaml[test] == nil {
IssuesYaml[test] = &YamlIssues{
Want: want,
Got: got,
YamlAnalyzer: yamlChecker,
}

} else {
IssuesYaml[test].Want = append(IssuesYaml[test].Want, want...)
IssuesYaml[test].Got = append(IssuesYaml[test].Got, got...)
IssuesYaml[test].YamlAnalyzer = yamlChecker
}
// if len(want) != len(got) {
// testName := filepath.Base(test.YamlCheckerPath)
// message := fmt.Sprintf(
// "(%s): expected issues on the following lines: %v\nbut issues were raised on lines: %v\n",
// testName,
// want,
// got,
// )
// fmt.Fprintf(os.Stderr, "%s", message)
// passed = false
// continue
// }
// for j := 0; j < len(want); j++ {
// if want[j] != got[j] {
// testName := filepath.Base(test.YamlCheckerPath)
// message := fmt.Sprintf(
// "(%s): expected issue on line %d, but next occurrence is on line %d\n",
// testName,
// want[j],
// got[j],
// )
// fmt.Fprintf(os.Stderr, "%s\n", message)
// passed = false
// }

// }
}

return passed, nil
return IssuesYaml, nil
}

func FindYamlTestFiles(testDir string) ([]YamlTestCase, error) {
Expand Down
8 changes: 4 additions & 4 deletions analysis/testrunner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,16 +204,16 @@ func TestFindYamlTestFiles(t *testing.T) {

func TestRunYamlTestsPass(t *testing.T) {
testDir := "testdata/yaml_tests/pass"
passed, err := RunYamlTests(testDir)
_, err := RunYamlTests(testDir)
assert.NoError(t, err)
assert.True(t, passed)
// assert.True(t, passed)
}

func TestRunYamlTestsFail(t *testing.T) {
testDir := "testdata/yaml_tests/fail"
passed, err := RunYamlTests(testDir)
_, err := RunYamlTests(testDir)
assert.NoError(t, err)
assert.False(t, passed)
// assert.False(t, passed)
}

// Helper function to compare maps
Expand Down
62 changes: 40 additions & 22 deletions analysis/yaml.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,27 +47,35 @@ type PathFilter struct {
IncludeGlobs []glob.Glob
}

type AnalysisFunction struct {
Name string `yaml:"name"`
Parameters map[string][]string `yaml:"parameters"`
Analyzer *Analyzer
}

type Yaml struct {
Language string `yaml:"language"`
Code string `yaml:"name"`
Message string `yaml:"message"`
Category Category `yaml:"category"`
Severity Severity `yaml:"severity"`
Pattern string `yaml:"pattern"`
Patterns []string `yaml:"patterns"`
Description string `yaml:"description"`
Exclude []string `yaml:"exclude,omitempty"`
Include []string `yaml:"include,omitempty"`
Filters []filterYaml `yaml:"filters,omitempty"`
PathFilter *pathFilterYaml `yaml:"path_filter,omitempty"`
Language string `yaml:"language"`
Code string `yaml:"name"`
Message string `yaml:"message"`
Category Category `yaml:"category"`
Severity Severity `yaml:"severity"`
Pattern string `yaml:"pattern"`
Patterns []string `yaml:"patterns"`
Description string `yaml:"description"`
Exclude []string `yaml:"exclude,omitempty"`
Include []string `yaml:"include,omitempty"`
Filters []filterYaml `yaml:"filters,omitempty"`
PathFilter *pathFilterYaml `yaml:"path_filter,omitempty"`
AnalysisFunction *AnalysisFunction `yaml:"analysisFunction,omitempty"`
}

type YamlAnalyzer struct {
Analyzer *Analyzer
Patterns []*sitter.Query
NodeFilter []NodeFilter
PathFilter *PathFilter
Message string
Analyzer *Analyzer
Patterns []*sitter.Query
NodeFilter []NodeFilter
PathFilter *PathFilter
Message string
AnalysisFunction *AnalysisFunction
}

// ReadFromFile reads a pattern checker definition from a YAML config file.
Expand All @@ -92,6 +100,15 @@ func ReadFromBytes(fileContent []byte) (Analyzer, YamlAnalyzer, error) {
return Analyzer{}, YamlAnalyzer{}, err
}

if checker.AnalysisFunction != nil {
name := checker.AnalysisFunction.Name
lang := DecodeLanguage(checker.Language)
if AnalysisFuncDirectory.Pool[name] == nil {
AnalysisFuncDirectory.Pool[name] = make(map[Language]*AnalysisFunction)
}
AnalysisFuncDirectory.Pool[name][lang] = checker.AnalysisFunction
}

var patterns []*sitter.Query
if checker.Pattern != "" {
pattern, err := sitter.NewQuery([]byte(checker.Pattern), lang.Grammar())
Expand Down Expand Up @@ -181,11 +198,12 @@ func ReadFromBytes(fileContent []byte) (Analyzer, YamlAnalyzer, error) {
}

yamlAnalyzer := &YamlAnalyzer{
Analyzer: &patternChecker,
Patterns: patterns,
NodeFilter: filters,
PathFilter: pathFilter,
Message: message,
Analyzer: &patternChecker,
Patterns: patterns,
NodeFilter: filters,
PathFilter: pathFilter,
Message: message,
AnalysisFunction: checker.AnalysisFunction,
}

patternChecker.Run = RunYamlAnalyzer(yamlAnalyzer)
Expand Down
59 changes: 59 additions & 0 deletions checkers/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,3 +100,62 @@ func RunAnalyzerTests(analyzerRegistry []Analyzer) (bool, []error) {

return passed, errors
}

func RunYamlAnalyzers(dir string) (passed bool, err error) {
issues, err := analysis.RunYamlTests(dir)
if err != nil {
return false, fmt.Errorf("error running yaml tests: %w", err)
}

passed = true

for test, yaml := range issues {

if yaml.YamlAnalyzer.AnalysisFunction != nil {
name := yaml.YamlAnalyzer.AnalysisFunction.Name
lang := yaml.YamlAnalyzer.Analyzer.Language
InitializeAnalysisFunctionDirectory(name, lang)
analysisFuncAnalyzer := yaml.YamlAnalyzer.AnalysisFunction.Analyzer
if analysisFuncAnalyzer == nil {
return false, fmt.Errorf("no analysis function found for %s in %v", name, lang)
}
funcIssues, err := analysis.RunAnalyzers(test.TestFile, []*analysis.Analyzer{analysisFuncAnalyzer}, nil)
if err != nil {
return false, fmt.Errorf("error running analysis function for %s: %w", name, err)
}
for _, issue := range funcIssues {
yaml.Got = append(yaml.Got, int(issue.Node.Range().StartPoint.Row)+1)
}
}

if len(yaml.Want) != len(yaml.Got) {
fmt.Println("Hmm... the number of issues raised is not as expected.")
testName := filepath.Base(test.YamlCheckerPath)
message := fmt.Sprintf(
"(%s): expected issues on the following lines: %v\nbut issues were raised on lines: %v\n",
testName,
yaml.Want,
yaml.Got,
)
fmt.Fprintf(os.Stderr, "%s", message)
passed = false
continue
}
for j := 0; j < len(yaml.Want); j++ {
if yaml.Want[j] != yaml.Got[j] {
testName := filepath.Base(test.YamlCheckerPath)
message := fmt.Sprintf(
"(%s): expected issue on line %d, but next occurrence is on line %d\n",
testName,
yaml.Want[j],
yaml.Got[j],
)
fmt.Fprintf(os.Stderr, "%s\n", message)
passed = false
}

}
}

return passed, nil
}
15 changes: 15 additions & 0 deletions checkers/checker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package checkers

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestYamlAnalyzers(t *testing.T) {
path := "./testdata"

passed, err := RunYamlAnalyzers(path)
assert.NoError(t, err)
assert.True(t, passed)
}
19 changes: 19 additions & 0 deletions checkers/functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package checkers

import (
"globstar.dev/analysis"
"globstar.dev/checkers/javascript"
)

func InitializeAnalysisFunctionDirectory(name string, language analysis.Language) {
// Find a way to automate the registration of analyzers than adding them manually
switch name {
case "taint":
switch language {
case analysis.LangJs:
javascript.JsTaintAnalyzer()
}
default:
return
}
}
Loading