Refactor how ignored issues are tracked

Track ignored issues using file location instead of a AST node. There are issues linked to a different AST node than the original node used to start the scan.

Signed-off-by: Cosmin Cojocar <gcojocar@adobe.com>
This commit is contained in:
Cosmin Cojocar 2023-10-13 14:04:21 +02:00 committed by Cosmin Cojocar
parent f338a98bf3
commit 0ec6cd95d7
5 changed files with 58 additions and 67 deletions

View file

@ -57,6 +57,12 @@ const aliasOfAllRules = "*"
var generatedCodePattern = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`) var generatedCodePattern = regexp.MustCompile(`^// Code generated .* DO NOT EDIT\.$`)
// ignoreLocation keeps the location of an ignored rule
type ignoreLocation struct {
file string
line string
}
// The Context is populated with data parsed from the source code as it is scanned. // The Context is populated with data parsed from the source code as it is scanned.
// It is passed through to all rule functions as they are called. Rules may use // It is passed through to all rule functions as they are called. Rules may use
// this data in conjunction with the encountered AST node. // this data in conjunction with the encountered AST node.
@ -69,7 +75,7 @@ type Context struct {
Root *ast.File Root *ast.File
Imports *ImportTracker Imports *ImportTracker
Config Config Config Config
Ignores []map[string][]issue.SuppressionInfo Ignores map[ignoreLocation]map[string][]issue.SuppressionInfo
PassedValues map[string]interface{} PassedValues map[string]interface{}
} }
@ -360,7 +366,7 @@ func (gosec *Analyzer) CheckAnalyzers(pkg *packages.Package) {
if result != nil { if result != nil {
if passIssues, ok := result.([]*issue.Issue); ok { if passIssues, ok := result.([]*issue.Issue); ok {
for _, iss := range passIssues { for _, iss := range passIssues {
gosec.updateIssues(iss, false, []issue.SuppressionInfo{}) gosec.updateIssues(iss)
} }
} }
} }
@ -521,10 +527,8 @@ func (gosec *Analyzer) ignore(n ast.Node) map[string]issue.SuppressionInfo {
// Visit runs the gosec visitor logic over an AST created by parsing go code. // Visit runs the gosec visitor logic over an AST created by parsing go code.
// Rule methods added with AddRule will be invoked as necessary. // Rule methods added with AddRule will be invoked as necessary.
func (gosec *Analyzer) Visit(n ast.Node) ast.Visitor { func (gosec *Analyzer) Visit(n ast.Node) ast.Visitor {
ignores, ok := gosec.updateIgnoredRules(n) // Update any potentially ignored rules at the node location
if !ok { gosec.updateIgnoredRules(n)
return gosec
}
// Using ast.File instead of ast.ImportSpec, so that we can track all imports at once. // Using ast.File instead of ast.ImportSpec, so that we can track all imports at once.
switch i := n.(type) { switch i := n.(type) {
@ -533,56 +537,55 @@ func (gosec *Analyzer) Visit(n ast.Node) ast.Visitor {
} }
for _, rule := range gosec.ruleset.RegisteredFor(n) { for _, rule := range gosec.ruleset.RegisteredFor(n) {
suppressions, ignored := gosec.updateSuppressions(rule.ID(), ignores)
issue, err := rule.Match(n, gosec.context) issue, err := rule.Match(n, gosec.context)
if err != nil { if err != nil {
file, line := GetLocation(n, gosec.context) file, line := GetLocation(n, gosec.context)
file = path.Base(file) file = path.Base(file)
gosec.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line) gosec.logger.Printf("Rule error: %v => %s (%s:%d)\n", reflect.TypeOf(rule), err, file, line)
} }
gosec.updateIssues(issue, ignored, suppressions) gosec.updateIssues(issue)
} }
return gosec return gosec
} }
func (gosec *Analyzer) updateIgnoredRules(n ast.Node) (map[string][]issue.SuppressionInfo, bool) { func (gosec *Analyzer) updateIgnoredRules(n ast.Node) {
if n == nil {
if len(gosec.context.Ignores) > 0 {
gosec.context.Ignores = gosec.context.Ignores[1:]
}
return nil, false
}
// Get any new rule exclusions.
ignoredRules := gosec.ignore(n) ignoredRules := gosec.ignore(n)
if len(ignoredRules) > 0 {
// Now create the union of exclusions. if gosec.context.Ignores == nil {
ignores := map[string][]issue.SuppressionInfo{} gosec.context.Ignores = make(map[ignoreLocation]map[string][]issue.SuppressionInfo)
if len(gosec.context.Ignores) > 0 {
for k, v := range gosec.context.Ignores[0] {
ignores[k] = v
} }
line := issue.GetLine(gosec.context.FileSet.File(n.Pos()), n)
ignoreLocation := ignoreLocation{
file: gosec.context.FileSet.File(n.Pos()).Name(),
line: line,
} }
current, ok := gosec.context.Ignores[ignoreLocation]
for ruleID, suppression := range ignoredRules { if !ok {
ignores[ruleID] = append(ignores[ruleID], suppression) current = map[string][]issue.SuppressionInfo{}
}
for r, s := range ignoredRules {
if current[r] == nil {
current[r] = []issue.SuppressionInfo{}
}
current[r] = append(current[r], s)
}
gosec.context.Ignores[ignoreLocation] = current
} }
// Push the new set onto the stack.
gosec.context.Ignores = append([]map[string][]issue.SuppressionInfo{ignores}, gosec.context.Ignores...)
return ignores, true
} }
func (gosec *Analyzer) updateSuppressions(id string, ignores map[string][]issue.SuppressionInfo) ([]issue.SuppressionInfo, bool) { func (gosec *Analyzer) getSuppressionsAtLineInFile(file string, line string, id string) ([]issue.SuppressionInfo, bool) {
// Check if all rules are ignored. ignores, ok := gosec.context.Ignores[ignoreLocation{file: file, line: line}]
generalSuppressions, generalIgnored := ignores[aliasOfAllRules] if !ok {
// Check if the specific rule is ignored ignores = make(map[string][]issue.SuppressionInfo)
ruleSuppressions, ruleIgnored := ignores[id] }
// Check if the rule was specifically suppressed at this location.
generalSuppressions, generalIgnored := ignores[aliasOfAllRules]
ruleSuppressions, ruleIgnored := ignores[id]
ignored := generalIgnored || ruleIgnored ignored := generalIgnored || ruleIgnored
suppressions := append(generalSuppressions, ruleSuppressions...) suppressions := append(generalSuppressions, ruleSuppressions...)
// Track external suppressions. // Track external suppressions of this rule.
if gosec.ruleset.IsRuleSuppressed(id) { if gosec.ruleset.IsRuleSuppressed(id) {
ignored = true ignored = true
suppressions = append(suppressions, issue.SuppressionInfo{ suppressions = append(suppressions, issue.SuppressionInfo{
@ -593,8 +596,9 @@ func (gosec *Analyzer) updateSuppressions(id string, ignores map[string][]issue.
return suppressions, ignored return suppressions, ignored
} }
func (gosec *Analyzer) updateIssues(issue *issue.Issue, ignored bool, suppressions []issue.SuppressionInfo) { func (gosec *Analyzer) updateIssues(issue *issue.Issue) {
if issue != nil { if issue != nil {
suppressions, ignored := gosec.getSuppressionsAtLineInFile(issue.File, issue.Line, issue.RuleID)
if gosec.showIgnored { if gosec.showIgnored {
issue.NoSec = ignored issue.NoSec = ignored
} }

View file

@ -743,25 +743,6 @@ var _ = Describe("Analyzer", func() {
Expect(issues[0].Suppressions[0].Justification).To(Equal("")) Expect(issues[0].Suppressions[0].Justification).To(Equal(""))
}) })
It("should track multiple suppressions if the violation is suppressed by both #nosec and #nosec RuleList", func() {
sample := testutils.SampleCodeG101[0]
source := sample.Code[0]
analyzer.LoadRules(rules.Generate(false, rules.NewRuleFilter(false, "G101")).RulesInfo())
nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close()
nosecSource := strings.Replace(source, "}", "} //#nosec G101 -- Justification", 1)
nosecSource = strings.Replace(nosecSource, "func", "//#nosec\nfunc", 1)
nosecPackage.AddFile("pwd.go", nosecSource)
err := nosecPackage.Build()
Expect(err).ShouldNot(HaveOccurred())
err = analyzer.Process(buildTags, nosecPackage.Path)
Expect(err).ShouldNot(HaveOccurred())
issues, _, _ := analyzer.Report()
Expect(issues).To(HaveLen(sample.Errors))
Expect(issues[0].Suppressions).To(HaveLen(2))
})
It("should not report an error if the rule is not included", func() { It("should not report an error if the rule is not included", func() {
sample := testutils.SampleCodeG101[0] sample := testutils.SampleCodeG101[0]
source := sample.Code[0] source := sample.Code[0]
@ -807,7 +788,7 @@ var _ = Describe("Analyzer", func() {
nosecPackage := testutils.NewTestPackage() nosecPackage := testutils.NewTestPackage()
defer nosecPackage.Close() defer nosecPackage.Close()
nosecSource := strings.Replace(source, "}", "} //#nosec G101 -- Justification", 1) nosecSource := strings.Replace(source, "password := \"f62e5bcda4fae4f82370da0c6f20697b8f8447ef\"", "password := \"f62e5bcda4fae4f82370da0c6f20697b8f8447ef\" //#nosec G101 -- Justification", 1)
nosecPackage.AddFile("pwd.go", nosecSource) nosecPackage.AddFile("pwd.go", nosecSource)
err := nosecPackage.Build() err := nosecPackage.Build()
Expect(err).ShouldNot(HaveOccurred()) Expect(err).ShouldNot(HaveOccurred())

View file

@ -187,7 +187,7 @@ func main() {
} }
outputPath := filepath.Join(dir, *outputFile) outputPath := filepath.Join(dir, *outputFile)
if err := os.WriteFile(outputPath, src, 0o644); err != nil { if err := os.WriteFile(outputPath, src, 0o644); err != nil /*#nosec G306*/ {
log.Fatalf("Writing output: %s", err) log.Fatalf("Writing output: %s", err)
} //#nosec G306 }
} }

View file

@ -178,11 +178,7 @@ func codeSnippetEndLine(node ast.Node, fobj *token.File) int64 {
// New creates a new Issue // New creates a new Issue
func New(fobj *token.File, node ast.Node, ruleID, desc string, severity, confidence Score) *Issue { func New(fobj *token.File, node ast.Node, ruleID, desc string, severity, confidence Score) *Issue {
name := fobj.Name() name := fobj.Name()
start, end := fobj.Line(node.Pos()), fobj.Line(node.End()) line := GetLine(fobj, node)
line := strconv.Itoa(start)
if start != end {
line = fmt.Sprintf("%d-%d", start, end)
}
col := strconv.Itoa(fobj.Position(node.Pos()).Column) col := strconv.Itoa(fobj.Position(node.Pos()).Column)
var code string var code string
@ -217,3 +213,13 @@ func (i *Issue) WithSuppressions(suppressions []SuppressionInfo) *Issue {
i.Suppressions = suppressions i.Suppressions = suppressions
return i return i
} }
// GetLine returns the line number of a given ast.Node
func GetLine(fobj *token.File, node ast.Node) string {
start, end := fobj.Line(node.Pos()), fobj.Line(node.End())
line := strconv.Itoa(start)
if start != end {
line = fmt.Sprintf("%d-%d", start, end)
}
return line
}

View file

@ -53,9 +53,9 @@ func (p *TestPackage) write() error {
return nil return nil
} }
for filename, content := range p.Files { for filename, content := range p.Files {
if e := os.WriteFile(filename, []byte(content), 0o644); e != nil { if e := os.WriteFile(filename, []byte(content), 0o644); e != nil /* #nosec G306 */ {
return e return e
} //#nosec G306 }
} }
p.onDisk = true p.onDisk = true
return nil return nil