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
16 changes: 16 additions & 0 deletions analysis/analysis_functions.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package analysis

func TaintRun(args ...interface{}) func(*Pass) (any, error) {
return func(pass *Pass) (any, error) {
sources := args[0].([]string)
sinks := args[1].([]string)

return NewTaintAnalyzer(sources, sinks), nil
}
}

func NewTaintAnalyzer(sources, sinks []string) *Analyzer {
analyzer := &Analyzer{}

return analyzer
}
59 changes: 59 additions & 0 deletions analysis/analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,65 @@ func RunAnalyzers(path string, analyzers []*Analyzer, fileFilter func(string) bo
return raisedIssues, nil
}

func RunAnalysisFunction(path string, analyzers []*Analyzer, fileFilter func(string) bool) ([]*Issue, error) {
raisedIssues := []*Issue{}
langAnalyzerMap := make(map[Language][]*Analyzer)

for _, analyzer := range analyzers {
langAnalyzerMap[analyzer.Language] = append(langAnalyzerMap[analyzer.Language], findAnalyzers(analyzer)...)
}

file, err := ParseFile(path)
if err != nil {
if err != ErrUnsupportedLanguage {
fmt.Println(err)
}
return raisedIssues, err
}

fileSkipInfo := GatherSkipInfo(file)

reportFunc := func(pass *Pass, node *sitter.Node, message string) {
raisedIssue := &Issue{
Id: &pass.Analyzer.Name,
Node: node,
Message: message,
Filepath: pass.FileContext.FilePath,
}

skipLines := fileSkipInfo
if !ContainsSkipcq(skipLines, raisedIssue) {
raisedIssues = append(raisedIssues, raisedIssue)
}
}

for _, analyzers := range langAnalyzerMap {
pass := &Pass{
FileContext: file,
Report: reportFunc,
ResultOf: make(map[*Analyzer]any),
ResultCache: make(map[*Analyzer]map[*ParseResult]any),
}

for _, analyzer := range analyzers {
pass.Analyzer = analyzer

result, err := analyzer.Run(pass)
if err != nil {
return raisedIssues, err
}

pass.ResultOf[analyzer] = result
if _, ok := pass.ResultCache[analyzer]; !ok {
pass.ResultCache[analyzer] = make(map[*ParseResult]any)
}
pass.ResultCache[analyzer][file] = result
}
}

return raisedIssues, nil
}

func ReportIssues(issues []*Issue, format string) ([]byte, error) {
switch format {
case "json":
Expand Down
62 changes: 62 additions & 0 deletions analysis/feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
type AnalysisFunction struct {
Name string
Parameters []reflect.Type
Run func(args ...interface{}) (Analyzer, error)
}

---
functions/run_taint_analysis.go
---
TaintAnalysisFunction := AnalysisFunction{
Name: "taint",
Parameters: []reflect.Type{
reflect.TypeOf([]string{}), // sources
reflect.TypeOf([]string{}), // sinks
},
Description: "Runs a taint analysis on the provided function and its parameters.",
Run: func(args ...interface{}) (Analyzer, error) {
sources := args[0].([]string)
sinks := args[1].([]string)

analyzer := NewTaintAnalyzer(sources, sinks)
return analyzer, nil
}
}

func NewTaintAnalyzer(sources, sinks []string) Analyzer {
return &TaintAnalyzer{
Sources: sources,
Sinks: sinks,
}
}

---
directory.go
---
functions := []AnalysisFunction{
TaintAnalysisFunction,
}

for _, function := range functions {
analyzer, err := function.Run(function.Parameters...)
analyzers = append(analyzers, analyzer)
}



---
name: "run_taint_analysis"
language: go
description: "Runs a taint analysis on the provided function and its parameters."
analysisFunction:
name: taint
parameters:
sources:
- (query)
sinks:
- (
(callexpression method @methodname (parameterList))
#match @methodname "get_user_input"
)
- (function (parameterList))
- (function (parameterList))
63 changes: 62 additions & 1 deletion analysis/language.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"os"
"path/filepath"
"strings"

sitter "github.com/smacker/go-tree-sitter"

Expand Down Expand Up @@ -83,6 +84,66 @@ const (
LangSwift
)

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
}
}

// tsGrammarForLang returns the tree-sitter grammar for the given language.
// May return `nil` when `lang` is `LangUnkown`.
func (lang Language) Grammar() *sitter.Language {
Expand Down Expand Up @@ -169,7 +230,7 @@ func LanguageFromFilePath(path string) Language {
return LangYaml
case ".css":
return LangCss
case ".dockerfile":
case ".dockerfile", ".Dockerfile":
return LangDockerfile
case ".md":
return LangMarkdown
Expand Down
2 changes: 1 addition & 1 deletion analysis/scope.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,9 +147,9 @@ func buildScopeTree(
if builder.NodeCreatesScope(node) {
nextScope = NewScope(scope)
scopeOfNode[node] = nextScope
scope.AstNode = node
if scope != nil {
scope.Children = append(scope.Children, nextScope)
scope.AstNode = node
} else {
scope = nextScope // root
}
Expand Down
11 changes: 11 additions & 0 deletions analysis/testdata/mock-analysis-function.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
function getUserInput(key) {

return document.getElementById(key).value;

}

userInput = getUserInput('username')

// A sink method, which performs some raw databse operation on the userInput
// <expect-error>
perform_db_operation(userInput)
23 changes: 23 additions & 0 deletions analysis/testdata/mock-analysis-function.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
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."
11 changes: 11 additions & 0 deletions analysis/testdata/mock-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
language: javascript
name: mock-checker
message: "This is just a mock checker"
category: style
severity: info
pattern:
(call_expression) @mock-checker
description: |
This is a mock checker.


9 changes: 9 additions & 0 deletions analysis/testdata/node-filter-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
language: javascript
name: node-filter-checker
message: "Variable @var found inside function"
category: style
severity: info
pattern: (variable_declarator) @var @node-filter-checker
filters:
- pattern-inside: (function_declaration)
description: "Check for variables declared inside functions"
10 changes: 10 additions & 0 deletions analysis/testdata/node-filter-test-checker.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
console.log("Hello, world!");

function foo(){
// <expect-error>
console.log("This should be detected");

/*
console.log("This Should not be detected");
*/
}
15 changes: 15 additions & 0 deletions analysis/testdata/node-filter-test-checker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
language: javascript
name: node-filter-test-checker
message: "Variable @var found inside function"
category: style
severity: info
pattern: >
(call_expression
function: (member_expression
object: (identifier) @obj
property: (property_identifier) @method
(#eq? @obj "console"))) @node-filter-test-checker
filters:
- pattern-inside: (function_declaration)
- pattern-not-inside: (comment)
description: "Check for variables declared inside functions"
7 changes: 7 additions & 0 deletions analysis/testrunner.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ func RunAnalyzerTests(testDir string, analyzers []*Analyzer) (string, string, bo

// if there's a test file in the testDir for which there's no analyzer,
// it's most likely a YAML checker test, so skip it

// yamlAnalyzers, err := discoverYamlAnalyzers(testDir)
// if err != nil {
// return "", "", false, err
// }
// analyzers = append(analyzers, yamlAnalyzers...)

likelyTestFiles := []string{}
for _, analyzer := range analyzers {
likelyTestFiles = append(likelyTestFiles, fmt.Sprintf("%s.test%s", analyzer.Name, GetExtFromLanguage(analyzer.Language)))
Expand Down
15 changes: 15 additions & 0 deletions analysis/walk.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package analysis

import (
"fmt"

sitter "github.com/smacker/go-tree-sitter"
)

Expand Down Expand Up @@ -94,3 +96,16 @@ func FirstChildOfType(node *sitter.Node, nodeType string) *sitter.Node {

return nil
}

func GetRootNode(node *sitter.Node) (*sitter.Node, error) {
current := node

if current.Parent() == nil {
return current, fmt.Errorf("at the top-most level for the node")
}
for current.Parent() != nil {
current = current.Parent()
}

return current, nil
}
Loading