From b114c937904f8f9fe20d23a2dbcb4b9bfc36fe65 Mon Sep 17 00:00:00 2001 From: Maharshi Basu Date: Wed, 23 Apr 2025 21:34:11 +0530 Subject: [PATCH 1/5] merge yaml runtime with go runtime --- analysis/yaml_analyzer.go | 425 ++++++++++++++++++++++++++++++++++++++ analysis/yaml_pattern.go | 154 ++++++++++++++ analysis/yaml_rule.go | 35 ++++ 3 files changed, 614 insertions(+) create mode 100644 analysis/yaml_analyzer.go create mode 100644 analysis/yaml_pattern.go create mode 100644 analysis/yaml_rule.go diff --git a/analysis/yaml_analyzer.go b/analysis/yaml_analyzer.go new file mode 100644 index 00000000..7b02a36c --- /dev/null +++ b/analysis/yaml_analyzer.go @@ -0,0 +1,425 @@ +package analysis + +import ( + sitter "github.com/smacker/go-tree-sitter" + "fmt" + "regexp" + "strings" + "encoding/json" + "path/filepath" +) + +type YamlIssue struct { + // The category of the issue + Category Category + // The severity of the issue + Severity Severity + // The message to display to the user + Message string + // The file path of the file that the issue was found in + Filepath string + // The range of the issue in the source code + Range sitter.Range + // (optional) The AST node that caused the issue + Node *sitter.Node + // Id is a unique ID for the issue. + // Issue that have 'Id's can be explained using the `globstar desc` command. + Id *string +} + +func (i *YamlIssue) AsJson() ([]byte, error) { + type location struct { + Row int `json:"row"` + Column int `json:"column"` + } + + type position struct { + Filename string `json:"filename"` + Start location `json:"start"` + End location `json:"end"` + } + + type issueJson struct { + Category Category `json:"category"` + Severity Severity `json:"severity"` + Message string `json:"message"` + Range position `json:"range"` + Id string `json:"id"` + } + issue := issueJson{ + Category: i.Category, + Severity: i.Severity, + Message: i.Message, + Range: position{ + Filename: i.Filepath, + Start: location{ + Row: int(i.Range.StartPoint.Row), + Column: int(i.Range.StartPoint.Column), + }, + End: location{ + Row: int(i.Range.EndPoint.Row), + Column: int(i.Range.EndPoint.Column), + }, + }, + Id: *i.Id, + } + + return json.Marshal(issue) +} + +func (i *YamlIssue) AsText() ([]byte, error) { + return []byte(fmt.Sprintf("%s:%d:%d:%s", i.Filepath, i.Range.StartPoint.Row, i.Range.StartPoint.Column, i.Message)), nil +} + +type YamlAnalyzer struct { + Language Language + // WorkDir is the directory in which the analysis is being run. + WorkDir string + // ParseResult is the result of parsing a file with a tree-sitter parser, + // along with some extra appendages (e.g: scope information). + ParseResult *ParseResult + // checkers is a list of all checkers that should be applied to the AST + // for this language. + checkers []Checker + // patternCheckers is a list of all checkers that run after a query is run on the AST. + // Usually, these are written in a DSL (which, for now, is the tree-sitter S-Expression query language) + YamlCheckers []YamlChecker + // entryCheckers maps node types to the checkers that should be applied + // when entering that node. + entryCheckersForNode map[string][]Checker + // exitCheckers maps node types to the checkers that should be applied + // when leaving that node. + exitCheckersForNode map[string][]Checker + issuesRaised []*YamlIssue +} + +func InitializeSkipComments(analyzers []*YamlAnalyzer) { + fileSkipComments := make(map[string][]*SkipComment) + + processedPaths := make(map[string]bool) + + for _, analyzer := range analyzers { + filepath := analyzer.ParseResult.FilePath + if processedPaths[filepath] { + continue + } + + processedPaths[filepath] = true + fileSkipComments[filepath] = GatherSkipInfo(analyzer.ParseResult) + } +} + +func FromFile(filePath string, baseCheckers []Checker) (*YamlAnalyzer, error) { + res, err := ParseFile(filePath) + if err != nil { + return nil, err + } + + return NewAnalyzer(res, baseCheckers), nil +} + +func NewAnalyzer(file *ParseResult, checkers []Checker) *YamlAnalyzer { + ana := &YamlAnalyzer{ + ParseResult: file, + Language: file.Language, + entryCheckersForNode: map[string][]Checker{}, + exitCheckersForNode: map[string][]Checker{}, + } + + for _, checker := range checkers { + ana.AddChecker(checker) + } + + return ana +} + +func (ana *YamlAnalyzer) Analyze() []*YamlIssue { + WalkTree(ana.ParseResult.Ast, ana) + ana.runPatternCheckers() + return ana.issuesRaised +} + +func (ana *YamlAnalyzer) AddChecker(checker Checker) { + ana.checkers = append(ana.checkers, checker) + typ := checker.NodeType() + + if checker.OnEnter() != nil { + ana.entryCheckersForNode[typ] = append(ana.entryCheckersForNode[typ], checker) + } + + if checker.OnLeave() != nil { + ana.exitCheckersForNode[typ] = append(ana.exitCheckersForNode[typ], checker) + } +} + +func (ana *YamlAnalyzer) OnEnterNode(node *sitter.Node) bool { + nodeType := node.Type() + checkers := ana.entryCheckersForNode[nodeType] + for _, checker := range checkers { + visitFn := checker.OnEnter() + if visitFn != nil { + (*visitFn)(checker, ana, node) + } + } + return true +} + +func (ana *YamlAnalyzer) OnLeaveNode(node *sitter.Node) { + nodeType := node.Type() + checkers := ana.exitCheckersForNode[nodeType] + for _, checker := range checkers { + visitFn := checker.OnLeave() + if visitFn != nil { + (*visitFn)(checker, ana, node) + } + } +} + +func (ana *YamlAnalyzer) shouldSkipChecker(checker YamlChecker) bool { + pathFilter := checker.PathFilter() + if pathFilter == nil { + // no filter is set, so we should not skip this checker + return false + } + + relPath := ana.ParseResult.FilePath + if ana.WorkDir != "" { + rel, err := filepath.Rel(ana.WorkDir, ana.ParseResult.FilePath) + if err == nil { + relPath = rel + } + } + + if len(pathFilter.ExcludeGlobs) > 0 { + for _, excludeGlob := range pathFilter.ExcludeGlobs { + if excludeGlob.Match(relPath) { + return true + } + } + + // no exclude globs matched, so we should not skip this checker + return false + } + + if len(pathFilter.IncludeGlobs) > 0 { + for _, includeGlob := range pathFilter.IncludeGlobs { + if includeGlob.Match(relPath) { + return false + } + } + + // no include globs matched, so we should skip this checker + return true + } + + return false +} + +func (ana *YamlAnalyzer) filterMatchesParent(filter *NodeFilter, parent *sitter.Node) bool { + qc := sitter.NewQueryCursor() + defer qc.Close() + + qc.Exec(filter.query, parent) + + // check if the filter matches the `parent` node + for { + m, ok := qc.NextMatch() + if !ok { + break + } + + m = qc.FilterPredicates(m, ana.ParseResult.Source) + for _, capture := range m.Captures { + captureName := filter.query.CaptureNameForId(capture.Index) + if captureName == filterPatternKey && capture.Node == parent { + return true + } + } + } + + return false +} + +func (ana *YamlAnalyzer) runParentFilters(checker YamlChecker, node *sitter.Node) bool { + filters := checker.NodeFilters() + if len(filters) == 0 { + return true + } + + for _, filter := range filters { + shouldMatch := filter.shouldMatch + nodeMatched := false + + // The matched node is expected to be a child of some other + // node, but it has no parents (is a top-level node) + if node.Parent() == nil && filter.shouldMatch { + return false + } + + for parent := node.Parent(); parent != nil; parent = parent.Parent() { + if ana.filterMatchesParent(&filter, parent) { + nodeMatched = true + if !shouldMatch { + // pattern-not-inside matched, so this checker should be skipped + return false + } else { + // pattern-inside matched, so we can break out of the loop + break + } + } + } + + if !nodeMatched && shouldMatch { + return false + } + } + + return true +} + +func (ana *YamlAnalyzer) executeCheckerQuery(checker YamlChecker, query *sitter.Query) { + qc := sitter.NewQueryCursor() + defer qc.Close() + + qc.Exec(query, ana.ParseResult.Ast) + for { + m, ok := qc.NextMatch() + + if !ok { + break + } + + m = qc.FilterPredicates(m, ana.ParseResult.Source) + for _, capture := range m.Captures { + captureName := query.CaptureNameForId(capture.Index) + // TODO: explain why captureName == checker.Name() + if captureName == checker.Name() && ana.runParentFilters(checker, capture.Node) { + checker.OnMatch(ana, query, capture.Node, m.Captures) + } + } + } +} + +// runPatternCheckers executes all checkers that are written as AST queries. +func (ana *YamlAnalyzer) runPatternCheckers() { + for _, checker := range ana.YamlCheckers { + if ana.shouldSkipChecker(checker) { + continue + } + + queries := checker.Patterns() + for _, q := range queries { + ana.executeCheckerQuery(checker, q) + } + } +} + +func (ana *YamlAnalyzer) Report(issue *YamlIssue) { + ana.issuesRaised = append(ana.issuesRaised, issue) +} + +func RunYamlCheckers(path string, analyzers []*YamlAnalyzer) ([]*YamlIssue, error) { + InitializeSkipComments(analyzers) + + issues := []*YamlIssue{} + for _, analyzer := range analyzers { + issues = append(issues, analyzer.Analyze()...) + } + return issues, nil +} + +func YamlGatherSkipInfo(fileContext *ParseResult) []*SkipComment { + var skipLines []*SkipComment + + commentIdentifier := GetEscapedCommentIdentifierFromPath(fileContext.FilePath) + pattern := fmt.Sprintf(`%s(?i).*?\bskipcq\b(?::(?:\s*(?P([A-Za-z\-_0-9]*(?:,\s*)?)+))?)?`, commentIdentifier) + skipRegexp := regexp.MustCompile(pattern) + + query, err := sitter.NewQuery([]byte("(comment) @skipcq"), fileContext.Language.Grammar()) + + if err != nil { + return skipLines + } + + cursor := sitter.NewQueryCursor() + cursor.Exec(query, fileContext.Ast) + + // gather all skipcq comment lines in a single pass + for { + m, ok := cursor.NextMatch() + if !ok { + break + } + + for _, capture := range m.Captures { + captureName := query.CaptureNameForId(capture.Index) + if captureName != "skipcq" { + continue + } + + commentNode := capture.Node + commentLine := int(commentNode.StartPoint().Row) + commentText := commentNode.Content(fileContext.Source) + + matches := skipRegexp.FindStringSubmatch(commentText) + if matches != nil { + issueIdsIdx := skipRegexp.SubexpIndex("issue_ids") + var checkerIds []string + + if issueIdsIdx != -1 && issueIdsIdx < len(matches) && matches[issueIdsIdx] != "" { + issueIdsIdx := matches[issueIdsIdx] + idSlice := strings.Split(issueIdsIdx, ",") + for _, id := range idSlice { + trimmedId := strings.TrimSpace(id) + if trimmedId != "" { + checkerIds = append(checkerIds, trimmedId) + } + } + } + + skipLines = append(skipLines, &SkipComment{ + CommentLine: commentLine, + CommentText: commentText, + CheckerIds: checkerIds, // will be empty for generic skipcq + }) + } + + } + } + + return skipLines +} + +func (ana *YamlAnalyzer) ContainsSkipcq(skipLines []*SkipComment, issue *YamlIssue) bool { + if len(skipLines) == 0 { + return false + } + + issueNode := issue.Node + nodeLine := int(issueNode.StartPoint().Row) + prevLine := nodeLine - 1 + + var checkerId string + if issue.Id != nil { + checkerId = *issue.Id + } + + for _, comment := range skipLines { + if comment.CommentLine != nodeLine && comment.CommentLine != prevLine { + continue + } + + if len(comment.CheckerIds) > 0 { + for _, id := range comment.CheckerIds { + if checkerId == id { + return true + } + } + } else { + return true + } + } + + return false +} + diff --git a/analysis/yaml_pattern.go b/analysis/yaml_pattern.go new file mode 100644 index 00000000..c356fff3 --- /dev/null +++ b/analysis/yaml_pattern.go @@ -0,0 +1,154 @@ +package analysis + +import ( + "github.com/gobwas/glob" + sitter "github.com/smacker/go-tree-sitter" + "strings" +) + +type NodeFilter struct { + query *sitter.Query + shouldMatch bool +} + +// PathFilter is a glob that can be applied to a PatternChecker to restrict +// the files that the checker is applied to. +type PathFilter struct { + ExcludeGlobs []glob.Glob + IncludeGlobs []glob.Glob +} + +type YamlChecker interface { + Name() string + Patterns() []*sitter.Query + Language() Language + Category() Category + Severity() Severity + OnMatch( + ana *YamlAnalyzer, // the analyzer instance + matchedQuery *sitter.Query, // the query that found an AST node + matchedNode *sitter.Node, // the AST node that matched the query + captures []sitter.QueryCapture, // list of captures made inside the query + ) + PathFilter() *PathFilter + NodeFilters() []NodeFilter +} + +type patternCheckerImpl struct { + language Language + patterns []*sitter.Query + issueMessage string + issueId string + category Category + severity Severity + pathFilter *PathFilter + filters []NodeFilter +} + +var fileSkipComment = make(map[string][]*SkipComment) + +func (r *patternCheckerImpl) Language() Language { + return r.language +} + +func (r *patternCheckerImpl) Patterns() []*sitter.Query { + return r.patterns +} + +func (r *patternCheckerImpl) OnMatch( + ana *YamlAnalyzer, + matchedQuery *sitter.Query, + matchedNode *sitter.Node, + captures []sitter.QueryCapture, +) { + + // replace all '@' with the corresponding capture value + message := r.issueMessage + // TODO: 1. escape '@' in the message, 2. use a more efficient way to replace + for strings.ContainsRune(message, '@') { + for _, capture := range captures { + captureName := matchedQuery.CaptureNameForId(capture.Index) + message = strings.ReplaceAll( + message, + "@"+captureName, + capture.Node.Content(ana.ParseResult.Source), + ) + } + } + raisedIssue := &YamlIssue{ + Range: matchedNode.Range(), + Node: matchedNode, + Message: message, + Filepath: ana.ParseResult.FilePath, + Category: r.Category(), + Severity: r.Severity(), + Id: &r.issueId, + } + + filepath := ana.ParseResult.FilePath + skipComments := fileSkipComment[filepath] + if !ana.ContainsSkipcq(skipComments, raisedIssue) { + ana.Report(raisedIssue) + } +} + +func (r *patternCheckerImpl) Name() string { + return r.issueId +} + +func (r *patternCheckerImpl) PathFilter() *PathFilter { + return r.pathFilter +} + +func (r *patternCheckerImpl) NodeFilters() []NodeFilter { + return r.filters +} + +func (r *patternCheckerImpl) Category() Category { + return r.category +} + +func (r *patternCheckerImpl) Severity() Severity { + return r.severity +} + +func CreatePatternChecker( + patterns []*sitter.Query, + language Language, + issueMessage string, + issueId string, + pathFilter *PathFilter, +) YamlChecker { + return &patternCheckerImpl{ + language: language, + patterns: patterns, + issueMessage: issueMessage, + issueId: issueId, + pathFilter: pathFilter, + } +} + +type filterYAML struct { + PatternInside string `yaml:"pattern-inside,omitempty"` + PatternNotInside string `yaml:"pattern-not-inside,omitempty"` +} + +type PatternCheckerFile struct { + Language string `yaml:"language"` + Code string `yaml:"name"` + Message string `yaml:"message"` + Category Category `yaml:"category"` + Severity Severity `yaml:"severity"` + // Pattern is a single pattern in the form of: + // pattern: (some_pattern) + // in the YAML file + Pattern string `yaml:"pattern,omitempty"` + // Patterns are ultiple patterns in the form of: + // pattern: (something) + // in the YAML file + Patterns []string `yaml:"patterns,omitempty"` + Description string `yaml:"description,omitempty"` + Filters []filterYAML `yaml:"filters,omitempty"` + Exclude []string `yaml:"exclude,omitempty"` + Include []string `yaml:"include,omitempty"` +} diff --git a/analysis/yaml_rule.go b/analysis/yaml_rule.go new file mode 100644 index 00000000..8b0070a5 --- /dev/null +++ b/analysis/yaml_rule.go @@ -0,0 +1,35 @@ +package analysis + +import sitter "github.com/smacker/go-tree-sitter" + +const filterPatternKey = "__filter__key__" + +type VisitFn func(checker Checker, analyzer *YamlAnalyzer, node *sitter.Node) + +type Checker interface { + NodeType() string + GetLanguage() Language + OnEnter() *VisitFn + OnLeave() *VisitFn +} + +type checkerImpl struct { + nodeType string + language Language + onEnter *VisitFn + onLeave *VisitFn +} + +func (r *checkerImpl) NodeType() string { return r.nodeType } +func (r *checkerImpl) GetLanguage() Language { return r.language } +func (r *checkerImpl) OnEnter() *VisitFn { return r.onEnter } +func (r *checkerImpl) OnLeave() *VisitFn { return r.onLeave } + +func CreateChecker(nodeType string, language Language, onEnter, onLeave *VisitFn) Checker { + return &checkerImpl{ + nodeType: nodeType, + language: language, + onEnter: onEnter, + onLeave: onLeave, + } +} From acaf465d4b500b43e746d392c6b6028dedd2828c Mon Sep 17 00:00:00 2001 From: Maharshi Basu Date: Mon, 28 Apr 2025 21:51:37 +0530 Subject: [PATCH 2/5] test: add test for checker definition parsing --- analysis/fixtures/checkers/test_checker.yml | 14 ++ analysis/yaml_analyzer_test.go | 22 +++ analysis/yaml_rule.go | 192 +++++++++++++++++++- 3 files changed, 227 insertions(+), 1 deletion(-) create mode 100644 analysis/fixtures/checkers/test_checker.yml create mode 100644 analysis/yaml_analyzer_test.go diff --git a/analysis/fixtures/checkers/test_checker.yml b/analysis/fixtures/checkers/test_checker.yml new file mode 100644 index 00000000..966683a4 --- /dev/null +++ b/analysis/fixtures/checkers/test_checker.yml @@ -0,0 +1,14 @@ +language: py +name: test_checker +message: "This is a test checker" +category: security +severity: warning + +pattern: > + (assert_statement + (comparison_operator + (identifier) + (identifier))) @test_checker + +description: > + This is a test checker used in unit tests. \ No newline at end of file diff --git a/analysis/yaml_analyzer_test.go b/analysis/yaml_analyzer_test.go new file mode 100644 index 00000000..ea038388 --- /dev/null +++ b/analysis/yaml_analyzer_test.go @@ -0,0 +1,22 @@ +package analysis + +import ( + "testing" + "path/filepath" + + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + + +func TestCreateYamlChecker(t *testing.T) { + t.Run("test is yaml checker created", func(t *testing.T) { + checker, err := ReadFromFile(filepath.Join("fixtures", "checkers", "test_checker.yml")) + require.Nil(t, err) + assert.Equal(t, checker.Name(), "test_checker") + assert.Equal(t, checker.Language(), LangPy) + require.Nil(t, checker.NodeFilters()) + require.Nil(t, checker.PathFilter()) + }) +} \ No newline at end of file diff --git a/analysis/yaml_rule.go b/analysis/yaml_rule.go index 8b0070a5..1a11688b 100644 --- a/analysis/yaml_rule.go +++ b/analysis/yaml_rule.go @@ -1,6 +1,14 @@ package analysis -import sitter "github.com/smacker/go-tree-sitter" +import ( + "fmt" + "os" + "strings" + + "gopkg.in/yaml.v3" + "github.com/gobwas/glob" + sitter "github.com/smacker/go-tree-sitter" +) const filterPatternKey = "__filter__key__" @@ -33,3 +41,185 @@ func CreateChecker(nodeType string, language Language, onEnter, onLeave *VisitFn onLeave: onLeave, } } + +// ReadFromFile reads a pattern checker definition from a YAML config file. +func ReadFromFile(filePath string) (YamlChecker, error) { + fileContent, err := os.ReadFile(filePath) + if err != nil { + return nil, err + } + + return ReadFromBytes(fileContent) +} + +func DecodeLanguage(language string) Language { + language = strings.ToLower(language) + switch language { + case "javascript", "js": + return LangJs + case "typescript", "ts": + return LangTs + case "jsx", "tsx": + return LangTsx + case "python", "py": + return LangPy + case "ocaml", "ml": + return LangOCaml + case "docker", "dockerfile": + return LangDockerfile + case "java": + return LangJava + case "kotlin", "kt": + return LangKotlin + case "rust", "rs": + return LangRust + case "ruby", "rb": + return LangRuby + case "lua": + return LangLua + case "yaml", "yml": + return LangYaml + case "sql": + return LangSql + case "css", "css3": + return LangCss + case "markdown", "md": + return LangMarkdown + case "sh", "bash": + return LangBash + case "csharp", "cs": + return LangCsharp + case "elixir", "ex": + return LangElixir + case "elm": + return LangElm + case "go": + return LangGo + case "groovy": + return LangGroovy + case "hcl", "tf": + return LangHcl + case "html": + return LangHtml + case "php": + return LangPhp + case "scala": + return LangScala + case "swift": + return LangSwift + default: + return LangUnknown + } +} + +// ReadFromBytes reads a pattern checker definition from bytes array +func ReadFromBytes(fileContent []byte) (YamlChecker, error) { + var checker PatternCheckerFile + if err := yaml.Unmarshal(fileContent, &checker); err != nil { + return nil, err + } + + lang := DecodeLanguage(checker.Language) + if lang == LangUnknown { + return nil, fmt.Errorf("unknown language code: '%s'", checker.Language) + } + + if checker.Code == "" { + return nil, fmt.Errorf("no name provided in checker definition") + } + + if checker.Message == "" { + return nil, fmt.Errorf("no message provided in checker '%s'", checker.Code) + } + + var patterns []*sitter.Query + if checker.Pattern != "" { + pattern, err := sitter.NewQuery([]byte(checker.Pattern), lang.Grammar()) + if err != nil { + return nil, err + } + patterns = append(patterns, pattern) + } else if len(checker.Patterns) > 0 { + for _, patternStr := range checker.Patterns { + pattern, err := sitter.NewQuery([]byte(patternStr), lang.Grammar()) + if err != nil { + return nil, err + } + patterns = append(patterns, pattern) + } + } else { + return nil, fmt.Errorf("no pattern provided in checker '%s'", checker.Code) + } + + if checker.Pattern != "" && len(checker.Patterns) > 0 { + return nil, fmt.Errorf("only one of 'pattern' or 'patterns' can be provided in a checker definition") + } + + // include and exclude patterns + var pathFilter *PathFilter + if checker.Exclude != nil || checker.Include != nil { + pathFilter = &PathFilter{ + ExcludeGlobs: make([]glob.Glob, 0, len(checker.Exclude)), + IncludeGlobs: make([]glob.Glob, 0, len(checker.Include)), + } + + for _, exclude := range checker.Exclude { + g, err := glob.Compile(exclude) + if err != nil { + return nil, err + } + pathFilter.ExcludeGlobs = append(pathFilter.ExcludeGlobs, g) + } + + for _, include := range checker.Include { + g, err := glob.Compile(include) + if err != nil { + return nil, err + } + pathFilter.IncludeGlobs = append(pathFilter.IncludeGlobs, g) + } + } + + // node filters + var filters []NodeFilter + if checker.Filters != nil { + for _, filter := range checker.Filters { + if filter.PatternInside != "" { + queryStr := filter.PatternInside + " @" + filterPatternKey + query, err := sitter.NewQuery([]byte(queryStr), lang.Grammar()) + if err != nil { + return nil, err + } + + filters = append(filters, NodeFilter{ + query: query, + shouldMatch: true, + }) + } + + if filter.PatternNotInside != "" { + queryStr := filter.PatternNotInside + " @" + filterPatternKey + query, err := sitter.NewQuery([]byte(queryStr), lang.Grammar()) + if err != nil { + return nil, err + } + + filters = append(filters, NodeFilter{ + query: query, + shouldMatch: false, + }) + } + } + } + + patternChecker := &patternCheckerImpl{ + language: lang, + patterns: patterns, + issueMessage: checker.Message, + issueId: checker.Code, + pathFilter: pathFilter, + filters: filters, + } + + return patternChecker, nil +} From aa6a84b34a4161e47dd632f1b703d61c8ba651ca Mon Sep 17 00:00:00 2001 From: Maharshi Basu Date: Tue, 29 Apr 2025 13:24:32 +0530 Subject: [PATCH 3/5] test: add test to check if target file gets parsed --- .../fixtures/checkers/test_checker.test.py | 6 +++ analysis/fixtures/checkers/test_checker.yml | 9 ++-- analysis/yaml_analyzer_test.go | 48 +++++++++++++++++-- 3 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 analysis/fixtures/checkers/test_checker.test.py diff --git a/analysis/fixtures/checkers/test_checker.test.py b/analysis/fixtures/checkers/test_checker.test.py new file mode 100644 index 00000000..5f0122d1 --- /dev/null +++ b/analysis/fixtures/checkers/test_checker.test.py @@ -0,0 +1,6 @@ +def myFunc(b: str) -> bool: + assert str != "hello" + return True + +b = 20 +assert b > 10 \ No newline at end of file diff --git a/analysis/fixtures/checkers/test_checker.yml b/analysis/fixtures/checkers/test_checker.yml index 966683a4..b2300bde 100644 --- a/analysis/fixtures/checkers/test_checker.yml +++ b/analysis/fixtures/checkers/test_checker.yml @@ -4,11 +4,8 @@ message: "This is a test checker" category: security severity: warning -pattern: > - (assert_statement - (comparison_operator - (identifier) - (identifier))) @test_checker +pattern: | + (assert_statement) -description: > +description: | This is a test checker used in unit tests. \ No newline at end of file diff --git a/analysis/yaml_analyzer_test.go b/analysis/yaml_analyzer_test.go index ea038388..3058bad5 100644 --- a/analysis/yaml_analyzer_test.go +++ b/analysis/yaml_analyzer_test.go @@ -1,9 +1,8 @@ package analysis import ( - "testing" "path/filepath" - + "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -19,4 +18,47 @@ func TestCreateYamlChecker(t *testing.T) { require.Nil(t, checker.NodeFilters()) require.Nil(t, checker.PathFilter()) }) -} \ No newline at end of file +} + +func TestParseTargetFile(t *testing.T) { + t.Run("test if target file is parsed", func(t *testing.T) { + checker, err := ReadFromFile(filepath.Join("fixtures", "checkers", "test_checker.yml")) + require.Nil(t, err) + require.NotNil(t, checker) + analyzer, err := FromFile(filepath.Join("fixtures", "checkers", "test_checker.test.py"), []Checker{}) + analyzer.YamlCheckers = append(analyzer.YamlCheckers, checker) + require.Nil(t, err) + }) +} + +// TODO: figure out it does not work +// func TestYamlAnalyzerRunner(t *testing.T) { +// t.Run("test if the yaml analyzer works", func(t *testing.T) { +// checker, err := ReadFromFile(filepath.Join("fixtures", "checkers", "test_checker.yml")) +// require.Nil(t, err) +// require.NotNil(t, checker) + +// fmt.Printf("Checker loaded: %s\n", checker.Name()) +// fmt.Println("Checker patterns: ", checker.Patterns()) +// analyzer, err := FromFile(filepath.Join("fixtures", "checkers", "test_checker.test.py"), []Checker{}) +// require.Nil(t, err) + +// cwd, err := os.Getwd() +// require.Nil(t, err) +// analyzer.WorkDir = filepath.Join(cwd, "fixtures", "checkers") +// analyzer.YamlCheckers = append(analyzer.YamlCheckers, checker) +// fmt.Println(analyzer) +// issues := analyzer.Analyze() +// fmt.Println(analyzer) + +// fmt.Println(issues) + +// assert.Equal(t, 2, len(issues)) +// issue := issues[0] +// require.Nil(t, err) +// require.NotNil(t, issue) +// assert.Equal(t, issue.Category, CategorySecurity) +// assert.Equal(t, issue.Severity, SeverityWarning) + +// }) +// } \ No newline at end of file From 121ee4f5b13e3f97cc260ecabdc9d55dbb59e6bf Mon Sep 17 00:00:00 2001 From: Maharshi Basu Date: Mon, 12 May 2025 11:25:30 +0530 Subject: [PATCH 4/5] test: add test to check if yaml analyzer works Signed-off-by: Maharshi Basu --- analysis/fixtures/checkers/test_checker.yml | 2 +- analysis/yaml_analyzer_test.go | 44 ++++++++------------- 2 files changed, 18 insertions(+), 28 deletions(-) diff --git a/analysis/fixtures/checkers/test_checker.yml b/analysis/fixtures/checkers/test_checker.yml index b2300bde..9885e2af 100644 --- a/analysis/fixtures/checkers/test_checker.yml +++ b/analysis/fixtures/checkers/test_checker.yml @@ -5,7 +5,7 @@ category: security severity: warning pattern: | - (assert_statement) + (assert_statement) @test_checker description: | This is a test checker used in unit tests. \ No newline at end of file diff --git a/analysis/yaml_analyzer_test.go b/analysis/yaml_analyzer_test.go index 3058bad5..a7657f42 100644 --- a/analysis/yaml_analyzer_test.go +++ b/analysis/yaml_analyzer_test.go @@ -31,34 +31,24 @@ func TestParseTargetFile(t *testing.T) { }) } -// TODO: figure out it does not work -// func TestYamlAnalyzerRunner(t *testing.T) { -// t.Run("test if the yaml analyzer works", func(t *testing.T) { -// checker, err := ReadFromFile(filepath.Join("fixtures", "checkers", "test_checker.yml")) -// require.Nil(t, err) -// require.NotNil(t, checker) +func TestYamlAnalyzerRunner(t *testing.T) { + t.Run("test if the yaml analyzer works", func(t *testing.T) { + testCheckerPath := filepath.Join("fixtures", "checkers", "test_checker.yml") + testFilePath := filepath.Join("fixtures", "checkers", "test_checker.test.py") -// fmt.Printf("Checker loaded: %s\n", checker.Name()) -// fmt.Println("Checker patterns: ", checker.Patterns()) -// analyzer, err := FromFile(filepath.Join("fixtures", "checkers", "test_checker.test.py"), []Checker{}) -// require.Nil(t, err) - -// cwd, err := os.Getwd() -// require.Nil(t, err) -// analyzer.WorkDir = filepath.Join(cwd, "fixtures", "checkers") -// analyzer.YamlCheckers = append(analyzer.YamlCheckers, checker) -// fmt.Println(analyzer) -// issues := analyzer.Analyze() -// fmt.Println(analyzer) + checker, err := ReadFromFile(testCheckerPath) + require.Nil(t, err, "should create checker") + require.NotNil(t, checker, "checker should exist") -// fmt.Println(issues) + analyzer, err := FromFile(testFilePath, []Checker{}) + require.Nil(t, err, "should create analyzer from test file") + require.NotNil(t, analyzer, "analyzer should not be nil") -// assert.Equal(t, 2, len(issues)) -// issue := issues[0] -// require.Nil(t, err) -// require.NotNil(t, issue) -// assert.Equal(t, issue.Category, CategorySecurity) -// assert.Equal(t, issue.Severity, SeverityWarning) + analyzer.WorkDir = filepath.Join("fixtures", "checkers") + analyzer.YamlCheckers = append(analyzer.YamlCheckers, checker) + + issues := analyzer.Analyze() + require.NotNil(t, issues, "issues should be raised") -// }) -// } \ No newline at end of file + }) +} \ No newline at end of file From 5ff6d5a2d14e798ee5ceaae88e8669b3686a2d1d Mon Sep 17 00:00:00 2001 From: Maharshi Basu Date: Mon, 12 May 2025 11:39:04 +0530 Subject: [PATCH 5/5] fix: address deepsource failing metrics --- analysis/yaml_analyzer.go | 4 ++-- analysis/yaml_pattern.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/analysis/yaml_analyzer.go b/analysis/yaml_analyzer.go index 7b02a36c..9e9e7be2 100644 --- a/analysis/yaml_analyzer.go +++ b/analysis/yaml_analyzer.go @@ -321,7 +321,7 @@ func (ana *YamlAnalyzer) Report(issue *YamlIssue) { func RunYamlCheckers(path string, analyzers []*YamlAnalyzer) ([]*YamlIssue, error) { InitializeSkipComments(analyzers) - issues := []*YamlIssue{} + var issues []*YamlIssue for _, analyzer := range analyzers { issues = append(issues, analyzer.Analyze()...) } @@ -390,7 +390,7 @@ func YamlGatherSkipInfo(fileContext *ParseResult) []*SkipComment { return skipLines } -func (ana *YamlAnalyzer) ContainsSkipcq(skipLines []*SkipComment, issue *YamlIssue) bool { +func ContainsSkipcqYaml(skipLines []*SkipComment, issue *YamlIssue) bool { if len(skipLines) == 0 { return false } diff --git a/analysis/yaml_pattern.go b/analysis/yaml_pattern.go index c356fff3..dbcf70a7 100644 --- a/analysis/yaml_pattern.go +++ b/analysis/yaml_pattern.go @@ -87,7 +87,7 @@ func (r *patternCheckerImpl) OnMatch( filepath := ana.ParseResult.FilePath skipComments := fileSkipComment[filepath] - if !ana.ContainsSkipcq(skipComments, raisedIssue) { + if ContainsSkipcqYaml(skipComments, raisedIssue) { ana.Report(raisedIssue) } }