Skip to content
Open
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
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ linters:
- nolintlint
- revive
- staticcheck
- testifylint
- typecheck
- unconvert
- unparam
Expand All @@ -40,6 +41,9 @@ linters-settings:
- all
- '-SA1019'

testifylint:
enable-all: true

revive:
rules:
- name: dot-imports
Expand Down
30 changes: 5 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ You may obtain a copy of the License [here](http://www.apache.org/licenses/LICEN
[![Downloads](https://img.shields.io/github/downloads/securego/gosec/total.svg)](https://github.com/securego/gosec/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/securego/gosec.svg)](https://hub.docker.com/r/securego/gosec/tags)
[![Slack](https://img.shields.io/badge/Slack-4A154B?style=for-the-badge&logo=slack&logoColor=white)](http://securego.slack.com)
[![go-recipes](https://raw.githubusercontent.com/nikolaydubina/go-recipes/main/badge.svg?raw=true)](https://github.com/nikolaydubina/go-recipes)

## Install

Expand Down Expand Up @@ -211,30 +212,9 @@ A number of global settings can be provided in a configuration file as follows:
$ gosec -conf config.json .
```

Also some rules accept configuration. For instance on rule `G104`, it is possible to define packages along with a list
of functions which will be skipped when auditing the not checked errors:
#### Rule Configuration

```JSON
{
"G104": {
"ioutil": ["WriteFile"]
}
}
```

You can also configure the hard-coded credentials rule `G101` with additional patterns, or adjust the entropy threshold:

```JSON
{
"G101": {
"pattern": "(?i)passwd|pass|password|pwd|secret|private_key|token",
"ignore_entropy": false,
"entropy_threshold": "80.0",
"per_char_threshold": "3.0",
"truncate": "32"
}
}
```
Some rules accept configuration flags as well; these flags are documented in [RULES.md](https://github.com/securego/gosec/blob/master/RULES.md).

#### Go version

Expand Down Expand Up @@ -308,7 +288,7 @@ func main() {
}

client := &http.Client{Transport: tr}
_, err := client.Get("https://golang.org/")
_, err := client.Get("https://go.dev/")
if err != nil {
fmt.Println(err)
}
Expand Down Expand Up @@ -355,7 +335,7 @@ comment.

### Build tags

gosec is able to pass your [Go build tags](https://golang.org/pkg/go/build/) to the analyzer.
gosec is able to pass your [Go build tags](https://pkg.go.dev/go/build/) to the analyzer.
They can be provided as a comma separated list as follows:

```bash
Expand Down
61 changes: 61 additions & 0 deletions RULES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Rule Documentation

## Rules accepting parameters

As [README.md](https://github.com/securego/gosec/blob/master/README.md) mentions, some rules can be configured by adding parameters to the gosec JSON config. Per rule configs are encoded as top level objects in the gosec config, with the rule ID (`Gxxx`) as the key.

Currently, the following rules accept parameters. This list is manually maintained; if you notice an omission please add it!

### G101

The hard-coded credentials rule `G101` can be configured with additional patterns, and the entropy threshold can be adjusted:

```JSON
{
"G101": {
"pattern": "(?i)passwd|pass|password|pwd|secret|private_key|token",
"ignore_entropy": false,
"entropy_threshold": "80.0",
"per_char_threshold": "3.0",
"truncate": "32"
}
}
```

### G104

The unchecked error value rule `G104` can be configured with additional functions that should be permitted to be called without checking errors.

```JSON
{
"G104": {
"ioutil": ["WriteFile"]
}
}
```

### G111

The HTTP Directory serving rule `G111` can be configured with a different regex for detecting potentially overly permissive servers. Note that this *replaces* the default pattern of `http\.Dir\("\/"\)|http\.Dir\('\/'\)`.

```JSON
{
"G111": {
"pattern": "http\\.Dir\\(\"\\\/\"\\)|http\\.Dir\\('\\\/'\\)"
}
}

```

### G301, G302, G306, G307

The various file and directory permission checking rules can be configured with a different maximum allowable file permission.

```JSON
{
"G301":"0o600",
"G302":"0o600",
"G306":"0o750",
"G307":"0o750"
}
```
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ inputs:

runs:
using: 'docker'
image: 'docker://securego/gosec:2.21.4'
image: 'docker://securego/gosec:2.22.0'
args:
- ${{ inputs.args }}

Expand Down
110 changes: 58 additions & 52 deletions analyzer.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
package gosec

import (
"errors"
"fmt"
"go/ast"
"go/build"
Expand Down Expand Up @@ -543,8 +544,8 @@ func (gosec *Analyzer) ParseErrors(pkg *packages.Package) error {
// AppendError appends an error to the file errors
func (gosec *Analyzer) AppendError(file string, err error) {
// Do not report the error for empty packages (e.g. files excluded from build with a tag)
r := regexp.MustCompile(`no buildable Go source files in`)
if r.MatchString(err.Error()) {
var noGoErr *build.NoGoError
if errors.As(err, &noGoErr) {
return
}
errors := make([]Error, 0)
Expand All @@ -558,66 +559,71 @@ func (gosec *Analyzer) AppendError(file string, err error) {

// ignore a node (and sub-tree) if it is tagged with a nosec tag comment
func (gosec *Analyzer) ignore(n ast.Node) map[string]issue.SuppressionInfo {
if groups, ok := gosec.context.Comments[n]; ok && !gosec.ignoreNosec {
if gosec.ignoreNosec {
return nil
}
groups, ok := gosec.context.Comments[n]
if !ok {
return nil
}

// Checks if an alternative for #nosec is set and, if not, uses the default.
noSecDefaultTag, err := gosec.config.GetGlobal(Nosec)
if err != nil {
noSecDefaultTag = NoSecTag(string(Nosec))
} else {
noSecDefaultTag = NoSecTag(noSecDefaultTag)
}
noSecAlternativeTag, err := gosec.config.GetGlobal(NoSecAlternative)
if err != nil {
noSecAlternativeTag = noSecDefaultTag
} else {
noSecAlternativeTag = NoSecTag(noSecAlternativeTag)
}
// Checks if an alternative for #nosec is set and, if not, uses the default.
noSecDefaultTag, err := gosec.config.GetGlobal(Nosec)
if err != nil {
noSecDefaultTag = NoSecTag(string(Nosec))
} else {
noSecDefaultTag = NoSecTag(noSecDefaultTag)
}
noSecAlternativeTag, err := gosec.config.GetGlobal(NoSecAlternative)
if err != nil {
noSecAlternativeTag = noSecDefaultTag
} else {
noSecAlternativeTag = NoSecTag(noSecAlternativeTag)
}

for _, group := range groups {
comment := strings.TrimSpace(group.Text())
foundDefaultTag := strings.HasPrefix(comment, noSecDefaultTag) || regexp.MustCompile("\n *"+noSecDefaultTag).MatchString(comment)
foundAlternativeTag := strings.HasPrefix(comment, noSecAlternativeTag) || regexp.MustCompile("\n *"+noSecAlternativeTag).MatchString(comment)
for _, group := range groups {
comment := strings.TrimSpace(group.Text())
foundDefaultTag := strings.HasPrefix(comment, noSecDefaultTag) || regexp.MustCompile("\n *"+noSecDefaultTag).MatchString(comment)
foundAlternativeTag := strings.HasPrefix(comment, noSecAlternativeTag) || regexp.MustCompile("\n *"+noSecAlternativeTag).MatchString(comment)

if foundDefaultTag || foundAlternativeTag {
gosec.stats.NumNosec++
if foundDefaultTag || foundAlternativeTag {
gosec.stats.NumNosec++

// Discard what's in front of the nosec tag.
if foundDefaultTag {
comment = strings.SplitN(comment, noSecDefaultTag, 2)[1]
} else {
comment = strings.SplitN(comment, noSecAlternativeTag, 2)[1]
}
// Discard what's in front of the nosec tag.
if foundDefaultTag {
comment = strings.SplitN(comment, noSecDefaultTag, 2)[1]
} else {
comment = strings.SplitN(comment, noSecAlternativeTag, 2)[1]
}

// Extract the directive and the justification.
justification := ""
commentParts := regexp.MustCompile(`-{2,}`).Split(comment, 2)
directive := commentParts[0]
if len(commentParts) > 1 {
justification = strings.TrimSpace(strings.TrimRight(commentParts[1], "\n"))
}
// Extract the directive and the justification.
justification := ""
commentParts := regexp.MustCompile(`-{2,}`).Split(comment, 2)
directive := commentParts[0]
if len(commentParts) > 1 {
justification = strings.TrimSpace(strings.TrimRight(commentParts[1], "\n"))
}

// Pull out the specific rules that are listed to be ignored.
re := regexp.MustCompile(`(G\d{3})`)
matches := re.FindAllStringSubmatch(directive, -1)
// Pull out the specific rules that are listed to be ignored.
re := regexp.MustCompile(`(G\d{3})`)
matches := re.FindAllStringSubmatch(directive, -1)

suppression := issue.SuppressionInfo{
Kind: "inSource",
Justification: justification,
}
suppression := issue.SuppressionInfo{
Kind: "inSource",
Justification: justification,
}

// Find the rule IDs to ignore.
ignores := make(map[string]issue.SuppressionInfo)
for _, v := range matches {
ignores[v[1]] = suppression
}
// Find the rule IDs to ignore.
ignores := make(map[string]issue.SuppressionInfo)
for _, v := range matches {
ignores[v[1]] = suppression
}

// If no specific rules were given, ignore everything.
if len(matches) == 0 {
ignores[aliasOfAllRules] = suppression
}
return ignores
// If no specific rules were given, ignore everything.
if len(matches) == 0 {
ignores[aliasOfAllRules] = suppression
}
return ignores
}
}
return nil
Expand Down
6 changes: 5 additions & 1 deletion analyzer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package gosec_test

import (
"errors"
"go/build"
"log"
"regexp"
"strings"
Expand Down Expand Up @@ -1311,7 +1312,10 @@ var _ = Describe("Analyzer", func() {

Context("when appending errors", func() {
It("should skip error for non-buildable packages", func() {
analyzer.AppendError("test", errors.New(`loading file from package "pkg/test": no buildable Go source files in pkg/test`))
err := &build.NoGoError{
Dir: "pkg/test",
}
analyzer.AppendError("test", err)
_, _, errors := analyzer.Report()
Expect(errors).To(BeEmpty())
})
Expand Down
35 changes: 15 additions & 20 deletions autofix/ai_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"

"github.com/securego/gosec/v2/issue"
)
Expand Down Expand Up @@ -44,17 +45,16 @@ func TestGenerateSolutionByGemini_Success(t *testing.T) {

mockClient := new(MockGenAIClient)
mockModel := new(MockGenAIGenerativeModel)
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel)
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("Autofix for issue 1", nil)
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel).Once()
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("Autofix for issue 1", nil).Once()

// Act
err := generateSolutionByGemini(mockClient, issues)

// Assert
assert.NoError(t, err)
assert.Equal(t, "Autofix for issue 1", issues[0].Autofix)
mockClient.AssertExpectations(t)
mockModel.AssertExpectations(t)
require.NoError(t, err)
assert.Equal(t, []*issue.Issue{{What: "Example issue 1", Autofix: "Autofix for issue 1"}}, issues)
mock.AssertExpectationsForObjects(t, mockClient, mockModel)
}

func TestGenerateSolutionByGemini_NoCandidates(t *testing.T) {
Expand All @@ -65,17 +65,15 @@ func TestGenerateSolutionByGemini_NoCandidates(t *testing.T) {

mockClient := new(MockGenAIClient)
mockModel := new(MockGenAIGenerativeModel)
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel)
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("", nil)
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel).Once()
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("", nil).Once()

// Act
err := generateSolutionByGemini(mockClient, issues)

// Assert
assert.Error(t, err)
assert.Equal(t, "no autofix returned by gemini", err.Error())
mockClient.AssertExpectations(t)
mockModel.AssertExpectations(t)
require.EqualError(t, err, "no autofix returned by gemini")
mock.AssertExpectationsForObjects(t, mockClient, mockModel)
}

func TestGenerateSolutionByGemini_APIError(t *testing.T) {
Expand All @@ -86,17 +84,15 @@ func TestGenerateSolutionByGemini_APIError(t *testing.T) {

mockClient := new(MockGenAIClient)
mockModel := new(MockGenAIGenerativeModel)
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel)
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("", errors.New("API error"))
mockClient.On("GenerativeModel", GeminiModel).Return(mockModel).Once()
mockModel.On("GenerateContent", mock.Anything, mock.Anything).Return("", errors.New("API error")).Once()

// Act
err := generateSolutionByGemini(mockClient, issues)

// Assert
assert.Error(t, err)
assert.Equal(t, "generating autofix with gemini: API error", err.Error())
mockClient.AssertExpectations(t)
mockModel.AssertExpectations(t)
require.EqualError(t, err, "generating autofix with gemini: API error")
mock.AssertExpectationsForObjects(t, mockClient, mockModel)
}

func TestGenerateSolution_UnsupportedProvider(t *testing.T) {
Expand All @@ -109,6 +105,5 @@ func TestGenerateSolution_UnsupportedProvider(t *testing.T) {
err := GenerateSolution("unsupported-provider", "test-api-key", "", issues)

// Assert
assert.Error(t, err)
assert.Equal(t, "ai provider not supported", err.Error())
require.EqualError(t, err, "ai provider not supported")
}
Loading