Skip to content

Commit 961019f

Browse files
authored
feature: Add SARIF file generation from result (#163)
* added sarif file generation * ⛵ fixed test * ⛵ extra coverage
1 parent f9971bc commit 961019f

17 files changed

+181
-23
lines changed

pathfinder-rules/android/WebViewJavaScriptEnabled.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-079
11+
* @ruleprovider android
1112
*/
1213
FROM method_invocation AS mi
1314
WHERE mi.getName() == "setJavaScriptEnabled" && "true" in mi.getArgumentName()

pathfinder-rules/android/WebViewaddJavascriptInterface.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-079
11+
* @ruleprovider android
1112
*/
1213

1314
predicate isJavaScriptEnabled(method_invocation mi) {

pathfinder-rules/android/WebViewsetAllowContentAccess.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-079
11+
* @ruleprovider android
1112
*/
1213
FROM method_invocation AS mi
1314
WHERE mi.getName() == "setAllowContentAccess" && "true" in mi.getArgumentName()

pathfinder-rules/android/WebViewsetAllowFileAccess.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-079
11+
* @ruleprovider android
1112
*/
1213
FROM method_invocation AS mi
1314
WHERE mi.getName() == "setAllowFileAccess" && "true" in mi.getArgumentName()

pathfinder-rules/android/WebViewsetAllowFileAccessFromFileURLs.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-079
11+
* @ruleprovider android
1112
*/
1213
FROM method_invocation AS mi
1314
WHERE mi.getName() == "setAllowFileAccessFromFileURLs" && "true" in mi.getArgumentName()

pathfinder-rules/java/BlowfishUsage.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-327
11+
* @ruleprovider java
1112
*/
1213

1314
FROM method_invocation AS mi

pathfinder-rules/java/DefaultHttpClient.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-326
11+
* @ruleprovider java
1112
*/
1213

1314
FROM ClassInstanceExpr AS cie

pathfinder-rules/java/InsecureRandom.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-330
11+
* @ruleprovider java
1112
*/
1213

1314
FROM method_invocation AS mi

pathfinder-rules/java/RC4Usage.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-327
11+
* @ruleprovider java
1112
*/
1213

1314
FROM method_invocation AS mi

pathfinder-rules/java/SHA1Usage.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-328
11+
* @ruleprovider java
1112
*/
1213

1314
FROM method_invocation AS mi

pathfinder-rules/java/UnEncryptedSocketConnection.cql

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* @precision medium
99
* @tags security
1010
* external/cwe/cwe-319
11+
* @ruleprovider java
1112
*/
1213

1314
FROM ClassInstanceExpr AS cie

sourcecode-parser/cmd/ci.go

Lines changed: 116 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,23 @@ import (
99
"path/filepath"
1010
"strings"
1111

12+
"github.com/owenrumney/go-sarif/v2/sarif"
13+
1214
"github.com/shivasurya/code-pathfinder/sourcecode-parser/graph"
1315

1416
"github.com/spf13/cobra"
1517
)
1618

19+
type Rule struct {
20+
ID string `json:"id"`
21+
Description string `json:"description"`
22+
Impact string `json:"impact"`
23+
Severity string `json:"severity"`
24+
Passed bool `json:"passed" default:"true"`
25+
Query string `json:"query"`
26+
RuleProvider string `json:"rule_provider"`
27+
}
28+
1729
var ciCmd = &cobra.Command{
1830
Use: "ci",
1931
Short: "Scan a project for vulnerabilities with ruleset in ci mode",
@@ -53,12 +65,13 @@ var ciCmd = &cobra.Command{
5365
for _, rule := range ruleset {
5466
queryInput := ParseQuery(rule)
5567
rulesetResult := make(map[string]interface{})
56-
result, err := processQuery(queryInput, codeGraph, output)
68+
result, err := processQuery(queryInput.Query, codeGraph, output)
5769

58-
if output == "json" {
70+
if output == "json" || output == "sarif" {
5971
var resultObject map[string]interface{}
6072
json.Unmarshal([]byte(result), &resultObject) //nolint:all
61-
rulesetResult["query"] = queryInput
73+
rulesetResult["query"] = queryInput.Query
74+
rulesetResult["rule"] = queryInput
6275
rulesetResult["result"] = resultObject
6376
outputResult = append(outputResult, rulesetResult)
6477
} else {
@@ -97,15 +110,74 @@ var ciCmd = &cobra.Command{
97110
fmt.Println("Error writing output file: ", err)
98111
}
99112
}
113+
} else if output == "sarif" {
114+
sarifReport, err := generateSarifReport(outputResult)
115+
if err != nil {
116+
fmt.Println("Error generating sarif report: ", err)
117+
os.Exit(1)
118+
}
119+
if graph.IsGitHubActions() {
120+
// append GITHUB_WORKSPACE to output file path
121+
outputFile = os.Getenv("GITHUB_WORKSPACE") + "/" + outputFile
122+
}
123+
if err := sarifReport.WriteFile(outputFile); err != nil {
124+
fmt.Println("Error writing sarif report: ", err)
125+
os.Exit(1)
126+
}
100127
}
101128
},
102129
}
103130

131+
func generateSarifReport(results []map[string]interface{}) (*sarif.Report, error) {
132+
report, err := sarif.New(sarif.Version210)
133+
if err != nil {
134+
return nil, err
135+
}
136+
run := sarif.NewRunWithInformationURI("CodePathFinder", "https://codepathfinder.dev")
137+
for _, result := range results {
138+
localresult := result["result"].(map[string]interface{}) //nolint:all
139+
resultSet := localresult["result_set"].([]interface{}) //nolint:all
140+
pb := sarif.NewPropertyBag()
141+
rule := result["rule"].(Rule) //nolint:all
142+
pb.Add("impact", rule.Impact)
143+
pb.Add("ruleProvider", rule.RuleProvider)
144+
145+
run.AddRule(rule.ID).
146+
WithDescription(rule.Description).
147+
WithProperties(pb.Properties).
148+
WithMarkdownHelp("# markdown")
149+
150+
for _, finding := range resultSet {
151+
findingMap := finding.(map[string]interface{}) //nolint:all
152+
file, _ := findingMap["file"].(string) //nolint:all
153+
line, _ := findingMap["line"].(float64) //nolint:all
154+
// convert line to int
155+
lineInt := int(line)
156+
157+
run.CreateResultForRule(rule.ID).
158+
WithLevel(strings.ToLower(rule.Severity)).
159+
WithMessage(sarif.NewTextMessage(rule.Description)).
160+
AddLocation(
161+
sarif.NewLocationWithPhysicalLocation(
162+
sarif.NewPhysicalLocation().
163+
WithArtifactLocation(
164+
sarif.NewSimpleArtifactLocation(file),
165+
).WithRegion(
166+
sarif.NewSimpleRegion(lineInt, lineInt),
167+
),
168+
),
169+
)
170+
}
171+
}
172+
report.AddRun(run)
173+
return report, nil
174+
}
175+
104176
func init() {
105177
rootCmd.AddCommand(ciCmd)
106-
ciCmd.Flags().StringP("output", "o", "", "Supported output format: json")
178+
ciCmd.Flags().StringP("output", "o", "", "Supported output format: json, sarif")
107179
ciCmd.Flags().StringP("output-file", "f", "", "Output file path")
108-
ciCmd.Flags().StringP("project", "p", "", "Project to analyze")
180+
ciCmd.Flags().StringP("project", "p", "", "Source code to analyze")
109181
ciCmd.Flags().StringP("ruleset", "r", "", "Ruleset to use example: cfp/java or directory path")
110182
}
111183

@@ -178,20 +250,55 @@ func downloadRuleset(ruleset string) ([]string, error) {
178250
return rules, nil
179251
}
180252

181-
func ParseQuery(query string) string {
253+
func ParseQuery(query string) Rule {
182254
// split query into lines
183255
lines := strings.Split(query, "\n")
184256
findLineFound := false
257+
commentLineFound := false
185258
query = ""
259+
comment := ""
260+
rule := Rule{}
186261
for _, line := range lines {
187262
// check if line starts with :
188-
if strings.HasPrefix(strings.TrimSpace(line), "predicate") || strings.HasPrefix(strings.TrimSpace(line), "FROM") {
263+
if strings.HasPrefix(strings.TrimSpace(line), "/*") { //nolint:all
264+
comment += line
265+
commentLineFound = true
266+
} else if strings.HasPrefix(strings.TrimSpace(line), "predicate") || strings.HasPrefix(strings.TrimSpace(line), "FROM") {
189267
findLineFound = true
190268
query += line + " "
191269
} else if findLineFound {
192270
query += line + " "
271+
} else if commentLineFound {
272+
comment += line
273+
key, value := ParseCommentLine(line)
274+
switch key {
275+
case "@id":
276+
rule.ID = value
277+
case "@description":
278+
rule.Description = value
279+
case "@problem.severity":
280+
rule.Severity = value
281+
case "@security-severity":
282+
rule.Impact = value
283+
case "@ruleprovider":
284+
rule.RuleProvider = value
285+
}
286+
} else if strings.HasPrefix(strings.TrimSpace(line), "*/") {
287+
commentLineFound = false
193288
}
194289
}
195-
query = strings.TrimSpace(query)
196-
return query
290+
rule.Query = strings.TrimSpace(query)
291+
return rule
292+
}
293+
294+
func ParseCommentLine(line string) (key, value string) {
295+
// parse comment start with "* @name <VALUE_MAY_CONTAIN_SPACE>"
296+
comment := strings.TrimSpace(line)
297+
comment = strings.TrimPrefix(comment, "*")
298+
comment = strings.TrimSpace(comment)
299+
parts := strings.Split(comment, " ")
300+
if len(parts) > 1 {
301+
return parts[0], strings.Join(parts[1:], " ")
302+
}
303+
return "", ""
197304
}

sourcecode-parser/cmd/ci_test.go

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ func TestCiCmd(t *testing.T) {
2020
{
2121
name: "Basic CI command",
2222
args: []string{"ci", "--help"},
23-
expectedOutput: "Scan a project for vulnerabilities with ruleset in ci mode\n\nUsage:\n pathfinder ci [flags]\n\nFlags:\n -h, --help help for ci\n -o, --output string Supported output format: json\n -f, --output-file string Output file path\n -p, --project string Project to analyze\n -r, --ruleset string Ruleset to use example: cfp/java or directory path\n",
23+
expectedOutput: "Scan a project for vulnerabilities with ruleset in ci mode\n\nUsage:\n pathfinder ci [flags]\n\nFlags:\n -h, --help help for ci\n -o, --output string Supported output format: json, sarif\n -f, --output-file string Output file path\n -p, --project string Source code to analyze\n -r, --ruleset string Ruleset to use example: cfp/java or directory path\n",
2424
},
2525
}
2626

@@ -64,37 +64,41 @@ func TestParseQuery(t *testing.T) {
6464
tests := []struct {
6565
name string
6666
input string
67-
expected string
67+
expected Rule
6868
}{
6969
{
7070
name: "Single predicate",
7171
input: "predicate foo()\n{\n bar\n}",
72-
expected: "predicate foo() { bar }",
72+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "predicate foo() { bar }", RuleProvider: ""},
7373
},
7474
{
7575
name: "Multiple predicates",
7676
input: "some code\npredicate foo()\n{\n bar\n}\npredicate baz()\n{\n qux\n}",
77-
expected: "predicate foo() { bar } predicate baz() { qux }",
78-
},
77+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "predicate foo() { bar } predicate baz() { qux }", RuleProvider: ""}},
7978
{
8079
name: "FROM clause",
8180
input: "SELECT *\nFROM table\nWHERE condition",
82-
expected: "FROM table WHERE condition",
81+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "FROM table WHERE condition", RuleProvider: ""},
8382
},
8483
{
8584
name: "Mixed predicates and FROM",
8685
input: "predicate foo()\n{\n bar\n}\nSELECT *\nFROM table\nWHERE condition",
87-
expected: "predicate foo() { bar } SELECT * FROM table WHERE condition",
86+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "predicate foo() { bar } SELECT * FROM table WHERE condition", RuleProvider: ""},
8887
},
8988
{
9089
name: "No matching lines",
91-
input: "Some random\ntext without\nmatching lines",
92-
expected: "",
90+
input: "cmd.Rule(cmd.Rule{ID:\"\", Description:\"\", Impact:\"\", Severity:\"\", Passed:false, Query:\"\", RuleProvider:\"\"})",
91+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "", RuleProvider: ""},
9392
},
9493
{
9594
name: "Empty input",
9695
input: "",
97-
expected: "",
96+
expected: Rule{ID: "", Description: "", Impact: "", Severity: "", Passed: false, Query: "", RuleProvider: ""},
97+
},
98+
{
99+
name: "Single line comment",
100+
input: "/**\n * @name Android WebView JavaScript settings\n * @description Enabling setAllowFileAccessFromFileURLs leak s&&box access to file:/// URLs.\n * @kind problem\n * @id java/Android/webview-javascript-enabled\n * @problem.severity warning\n * @security-severity 6.1\n * @precision medium\n * @tags security\n * external/cwe/cwe-079\n * @ruleprovider android\n */\nFROM method_invocation AS mi\nWHERE mi.getName() == \"setAllowFileAccessFromFileURLs\" && \"true\" in mi.getArgumentName()\nSELECT mi.getName(), \"File access enabled\"",
101+
expected: Rule{ID: "java/Android/webview-javascript-enabled", Description: "Enabling setAllowFileAccessFromFileURLs leak s&&box access to file:/// URLs.", Impact: "6.1", Severity: "warning", Passed: false, Query: "FROM method_invocation AS mi WHERE mi.getName() == \"setAllowFileAccessFromFileURLs\" && \"true\" in mi.getArgumentName() SELECT mi.getName(), \"File access enabled\"", RuleProvider: "android"},
98102
},
99103
}
100104

sourcecode-parser/cmd/query.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ func processQuery(input string, codeGraph *graph.CodeGraph, output string) (stri
128128
parsedQuery.Expression = strings.SplitN(parts[1], "SELECT", 2)[0]
129129
}
130130
entities, formattedOutput := graph.QueryEntities(codeGraph, parsedQuery)
131-
if output == "json" {
131+
if output == "json" || output == "sarif" {
132132
analytics.ReportEvent(analytics.QueryCommandJSON)
133133
// convert struct to query_results
134134
results := make(map[string]interface{})

sourcecode-parser/go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ require (
1515

1616
require (
1717
github.com/fatih/color v1.17.0
18+
github.com/owenrumney/go-sarif/v2 v2.3.3
1819
github.com/stretchr/testify v1.9.0
1920
)
2021

0 commit comments

Comments
 (0)