Skip to content

Commit 516260a

Browse files
authored
Fix #1240: nosec comments now work with trailing open brackets (#1475)
Add line-based fallback for nosec comment detection when ast.CommentMap fails to associate comments correctly. This fixes the case where #nosec comments at the end of lines with open brackets were being ignored. Refactor duplicate parsing logic into parseNoSecDirective helper function to reduce code complexity and improve maintainability.
1 parent be0fd6d commit 516260a

2 files changed

Lines changed: 206 additions & 32 deletions

File tree

analyzer.go

Lines changed: 76 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ func (i ignores) get(file string, line string) map[string][]issue.SuppressionInf
144144
type Context struct {
145145
FileSet *token.FileSet
146146
Comments ast.CommentMap
147+
AllComments []*ast.CommentGroup // All comments in the file for line-based nosec lookup (fix for #1240)
147148
Info *types.Info
148149
Pkg *types.Package
149150
PkgFiles []*ast.File
@@ -475,6 +476,7 @@ func (gosec *Analyzer) checkRules(pkg *packages.Package) ([]*issue.Issue, *Metri
475476
FileSet: pkg.Fset,
476477
Config: gosec.config,
477478
Comments: ast.NewCommentMap(pkg.Fset, file, file.Comments),
479+
AllComments: file.Comments, // Store all comments for line-based lookup (fix for #1240)
478480
Root: file,
479481
Info: pkg.TypesInfo,
480482
Pkg: pkg.Types,
@@ -800,15 +802,39 @@ func (v *astVisitor) updateIgnoredRulesForNode(n ast.Node) {
800802
}
801803
}
802804

805+
// parseNoSecDirective extracts rules and justification from nosec directive arguments
806+
func parseNoSecDirective(args string) map[string]issue.SuppressionInfo {
807+
justification := ""
808+
commentParts := regexp.MustCompile(`-{2,}`).Split(args, 2)
809+
directive := commentParts[0]
810+
if len(commentParts) > 1 {
811+
justification = strings.TrimSpace(strings.TrimRight(commentParts[1], "\n"))
812+
}
813+
814+
re := regexp.MustCompile(`(G\d{3})`)
815+
matches := re.FindAllStringSubmatch(directive, -1)
816+
817+
suppression := issue.SuppressionInfo{
818+
Kind: "inSource",
819+
Justification: justification,
820+
}
821+
822+
ignores := make(map[string]issue.SuppressionInfo)
823+
if len(matches) == 0 {
824+
ignores[aliasOfAllRules] = suppression
825+
} else {
826+
for _, v := range matches {
827+
ignores[v[1]] = suppression
828+
}
829+
}
830+
return ignores
831+
}
832+
803833
// ignore checks if a node is tagged with a nosec comment and returns the suppressed rules.
804834
func (v *astVisitor) ignore(n ast.Node) map[string]issue.SuppressionInfo {
805835
if v.ignoreNosec {
806836
return nil
807837
}
808-
groups, ok := v.context.Comments[n]
809-
if !ok {
810-
return nil
811-
}
812838

813839
noSecDefaultTag, err := v.gosec.config.GetGlobal(Nosec)
814840
if err != nil {
@@ -823,38 +849,56 @@ func (v *astVisitor) ignore(n ast.Node) map[string]issue.SuppressionInfo {
823849
noSecAlternativeTag = NoSecTag(noSecAlternativeTag)
824850
}
825851

826-
for _, group := range groups {
827-
found, args := findNoSecDirective(group, noSecDefaultTag, noSecAlternativeTag)
828-
if !found {
829-
continue
830-
}
831-
v.stats.NumNosec++
832-
833-
justification := ""
834-
commentParts := regexp.MustCompile(`-{2,}`).Split(args, 2)
835-
directive := commentParts[0]
836-
if len(commentParts) > 1 {
837-
justification = strings.TrimSpace(strings.TrimRight(commentParts[1], "\n"))
838-
}
839-
840-
re := regexp.MustCompile(`(G\d{3})`)
841-
matches := re.FindAllStringSubmatch(directive, -1)
842-
843-
suppression := issue.SuppressionInfo{
844-
Kind: "inSource",
845-
Justification: justification,
846-
}
852+
// First, try the existing AST CommentMap lookup
853+
groups, ok := v.context.Comments[n]
854+
if ok {
855+
for _, group := range groups {
856+
found, args := findNoSecDirective(group, noSecDefaultTag, noSecAlternativeTag)
857+
if !found {
858+
continue
859+
}
860+
v.stats.NumNosec++
861+
return parseNoSecDirective(args)
862+
}
863+
}
864+
865+
// FIX FOR ISSUE #1240:
866+
// If not found in CommentMap, check all comments on the same line.
867+
// This handles the case where the comment is associated with a parent node
868+
// (like IfStmt or BlockStmt) instead of the specific expression that
869+
// triggers the rule (like a type conversion CallExpr).
870+
//
871+
// Go's ast.CommentMap associates comments with the "largest" node on the
872+
// same line. For example:
873+
// if len(x) <= int(y) { // #nosec G115
874+
// The comment gets associated with the IfStmt, not the int(y) CallExpr.
875+
// This fallback ensures the nosec still applies to the conversion.
876+
if v.context.FileSet != nil && n != nil && v.context.AllComments != nil {
877+
nodePos := v.context.FileSet.Position(n.Pos())
878+
nodeEndPos := v.context.FileSet.Position(n.End())
879+
880+
for _, group := range v.context.AllComments {
881+
if group == nil {
882+
continue
883+
}
847884

848-
ignores := make(map[string]issue.SuppressionInfo)
849-
for _, v := range matches {
850-
ignores[v[1]] = suppression
851-
}
885+
for _, comment := range group.List {
886+
commentPos := v.context.FileSet.Position(comment.Pos())
852887

853-
if len(matches) == 0 {
854-
ignores[aliasOfAllRules] = suppression
888+
// Check if the comment is on the same line as the node
889+
// (either where it starts or ends, to handle multi-line nodes)
890+
if commentPos.Line == nodePos.Line || commentPos.Line == nodeEndPos.Line {
891+
found, args := findNoSecDirective(group, noSecDefaultTag, noSecAlternativeTag)
892+
if !found {
893+
continue
894+
}
895+
v.stats.NumNosec++
896+
return parseNoSecDirective(args)
897+
}
898+
}
855899
}
856-
return ignores
857900
}
901+
858902
return nil
859903
}
860904

analyzer_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2303,4 +2303,134 @@ func main() {
23032303
Expect(issues[0].Suppressions[0].Justification).To(Equal("false positive, this is not a private data"))
23042304
})
23052305
})
2306+
2307+
Context("when fixing issue #1240 - nosec with open bracket", func() {
2308+
It("should suppress G115 when #nosec is at the end of an if line with bracket", func() {
2309+
source := `
2310+
package main
2311+
2312+
import "fmt"
2313+
2314+
func main() {
2315+
ten := 10
2316+
uintTen := uint(10)
2317+
configVal := uint(ten) // #nosec G115 -- this works
2318+
inputSlice := []int{1, 2, 3, 4, 5}
2319+
2320+
if len(inputSlice) <= int(uintTen) { // #nosec G115 -- this works
2321+
fmt.Println("hello world!")
2322+
}
2323+
2324+
if len(inputSlice) <= int(configVal) { // #nosec G115 -- this should work now (fix for #1240)
2325+
fmt.Println("hello world!")
2326+
}
2327+
}
2328+
`
2329+
analyzer.LoadAnalyzers(analyzers.Generate(false, analyzers.NewAnalyzerFilter(false, "G115")).AnalyzersInfo())
2330+
pkg := testutils.NewTestPackage()
2331+
defer pkg.Close()
2332+
pkg.AddFile("main.go", source)
2333+
err := pkg.Build()
2334+
Expect(err).ShouldNot(HaveOccurred())
2335+
err = analyzer.Process(buildTags, pkg.Path)
2336+
Expect(err).ShouldNot(HaveOccurred())
2337+
issues, metrics, _ := analyzer.Report()
2338+
// No G115 issues should be reported as all conversions are suppressed
2339+
for _, issue := range issues {
2340+
if issue.RuleID == "G115" {
2341+
Fail(fmt.Sprintf("G115 should be suppressed but was reported at line %s", issue.Line))
2342+
}
2343+
}
2344+
Expect(metrics.NumNosec).Should(BeNumerically(">=", 3)) // At least 3 nosec comments
2345+
})
2346+
2347+
It("should suppress G115 when #nosec is used with block comment before bracket", func() {
2348+
source := `
2349+
package main
2350+
2351+
import "fmt"
2352+
2353+
func main() {
2354+
configVal := uint(10)
2355+
inputSlice := []int{1, 2, 3, 4, 5}
2356+
2357+
if len(inputSlice) <= int(configVal) /* #nosec G115 */ {
2358+
fmt.Println("hello world!")
2359+
}
2360+
}
2361+
`
2362+
analyzer.LoadAnalyzers(analyzers.Generate(false, analyzers.NewAnalyzerFilter(false, "G115")).AnalyzersInfo())
2363+
pkg := testutils.NewTestPackage()
2364+
defer pkg.Close()
2365+
pkg.AddFile("main.go", source)
2366+
err := pkg.Build()
2367+
Expect(err).ShouldNot(HaveOccurred())
2368+
err = analyzer.Process(buildTags, pkg.Path)
2369+
Expect(err).ShouldNot(HaveOccurred())
2370+
issues, _, _ := analyzer.Report()
2371+
// No G115 issues should be reported
2372+
for _, issue := range issues {
2373+
if issue.RuleID == "G115" {
2374+
Fail(fmt.Sprintf("G115 should be suppressed but was reported at line %s", issue.Line))
2375+
}
2376+
}
2377+
})
2378+
2379+
It("should suppress G115 in for loop with bracket and trailing comment", func() {
2380+
source := `
2381+
package main
2382+
2383+
func main() {
2384+
x := uint(10)
2385+
for i := 0; i < int(x); i++ { // #nosec G115
2386+
println(i)
2387+
}
2388+
}
2389+
`
2390+
analyzer.LoadAnalyzers(analyzers.Generate(false, analyzers.NewAnalyzerFilter(false, "G115")).AnalyzersInfo())
2391+
pkg := testutils.NewTestPackage()
2392+
defer pkg.Close()
2393+
pkg.AddFile("main.go", source)
2394+
err := pkg.Build()
2395+
Expect(err).ShouldNot(HaveOccurred())
2396+
err = analyzer.Process(buildTags, pkg.Path)
2397+
Expect(err).ShouldNot(HaveOccurred())
2398+
issues, _, _ := analyzer.Report()
2399+
// No G115 issues should be reported
2400+
for _, issue := range issues {
2401+
if issue.RuleID == "G115" {
2402+
Fail(fmt.Sprintf("G115 should be suppressed but was reported at line %s", issue.Line))
2403+
}
2404+
}
2405+
})
2406+
2407+
It("should suppress G115 in switch statement with bracket and trailing comment", func() {
2408+
source := `
2409+
package main
2410+
2411+
func main() {
2412+
x := uint(10)
2413+
switch int(x) { // #nosec G115
2414+
case 10:
2415+
println("ten")
2416+
}
2417+
}
2418+
`
2419+
analyzer.LoadAnalyzers(analyzers.Generate(false, analyzers.NewAnalyzerFilter(false, "G115")).AnalyzersInfo())
2420+
pkg := testutils.NewTestPackage()
2421+
defer pkg.Close()
2422+
pkg.AddFile("main.go", source)
2423+
err := pkg.Build()
2424+
Expect(err).ShouldNot(HaveOccurred())
2425+
err = analyzer.Process(buildTags, pkg.Path)
2426+
Expect(err).ShouldNot(HaveOccurred())
2427+
issues, _, _ := analyzer.Report()
2428+
// No G115 issues should be reported
2429+
for _, issue := range issues {
2430+
if issue.RuleID == "G115" {
2431+
Fail(fmt.Sprintf("G115 should be suppressed but was reported at line %s", issue.Line))
2432+
}
2433+
}
2434+
})
2435+
})
23062436
})

0 commit comments

Comments
 (0)