Compare commits

...

10 Commits

Author SHA1 Message Date
oittaa
8a5404eabf feat(resolve): enhance TryResolve to handle KeyValueExpr, IndexExpr, and SliceExpr (#1452)
* feat(resolve): enhance TryResolve to handle KeyValueExpr, IndexExpr, and SliceExpr

* golangci-lint
2026-01-04 17:22:20 +02:00
oittaa
0f6f21cb3f feat: add secrets serialization G117 (#1451)
* Rule to detect secrets serialization

* Add G117 to rules_test.go

* Fix false positives

* Map to CWE 499, update README
2026-01-04 17:21:22 +02:00
oittaa
717706e815 feat(rules): add support for detecting high entropy strings in composite literals (#1447) 2026-01-02 09:58:08 +02:00
oittaa
082deb6cee whitelist crypto/rand Read from error checks (#1446) 2025-12-31 18:57:36 +02:00
renovate[bot]
095d529a90 chore(deps): update all dependencies (#1443)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-30 10:18:39 +02:00
Cosmin Cojocar
c073629009 Improve slice bound check (#1442)
Improve slice bound check to habdle bounded values and properly parse
the address index only from references

Signed-off-by: Cosmin Cojocar <cosmin@cojocar.ch>
2025-12-28 19:39:40 +02:00
Ranyodh Singh
538a05cc5d docs: add documentation for using gosec with private modules (#1441)
* docs: add documentation for using gosec with private modules

Add a new section in the GitHub Action documentation explaining
how to configure gosec to work with projects that import private
Go modules.
This includes setting `GOPRIVATE` and `GITHUB_AUTHENTICATION_TOKEN`
environment variables.

* Update README.md
2025-12-19 09:41:56 +01:00
renovate[bot]
25804378cd chore(deps): update all dependencies (#1440)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2025-12-16 09:22:49 +01:00
kfess
872b33106c docs: add G116 rule description to README (#1439) 2025-12-16 09:22:27 +01:00
Cosmin Cojocar
dcf93a8b8b Update GitHub action to gosec 2.22.11 (#1438)
Signed-off-by: Cosmin Cojocar <ccojocar@google.com>
2025-12-11 15:27:06 +01:00
20 changed files with 719 additions and 62 deletions

View File

@@ -25,7 +25,7 @@ jobs:
go-version: ${{ matrix.version.go-version }}
- name: Checkout Source
uses: actions/checkout@v6
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
@@ -55,7 +55,7 @@ jobs:
go-version: "1.25.5"
- name: Checkout Source
uses: actions/checkout@v6
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ~/go/pkg/mod
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}

View File

@@ -76,6 +76,38 @@ jobs:
args: ./...
```
#### Scanning Projects with Private Modules
If your project imports private Go modules, you need to configure authentication so that `gosec` can fetch the dependencies. Set the following environment variables in your workflow:
- `GOPRIVATE`: A comma-separated list of module path prefixes that should be considered private (e.g., `github.com/your-org/*`).
- `GITHUB_AUTHENTICATION_TOKEN`: A GitHub token with read access to your private repositories.
```yaml
name: Run Gosec
on:
push:
branches:
- master
pull_request:
branches:
- master
jobs:
tests:
runs-on: ubuntu-latest
env:
GO111MODULE: on
GOPRIVATE: github.com/your-org/*
GITHUB_AUTHENTICATION_TOKEN: ${{ secrets.PRIVATE_REPO_TOKEN }}
steps:
- name: Checkout Source
uses: actions/checkout@v3
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: ./...
```
### Integrating with code scanning
You can [integrate third-party code analysis tools](https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/integrating-with-code-scanning) with GitHub code scanning by uploading data as SARIF files.
@@ -140,6 +172,8 @@ directory you can supply `./...` as the input argument.
- G112: Potential slowloris attack
- G114: Use of net/http serve function that has no support for setting timeouts
- G115: Potential integer overflow when converting between integer types
- G116: Detect Trojan Source attacks using bidirectional Unicode control characters
- G117: Potential exposure of secrets via JSON marshaling
- G201: SQL query construction using format string
- G202: SQL query construction using string concatenation
- G203: Use of unescaped data in HTML templates

View File

@@ -10,7 +10,7 @@ inputs:
runs:
using: "docker"
image: "docker://securego/gosec:2.22.10"
image: "docker://securego/gosec:2.22.11"
args:
- ${{ inputs.args }}

View File

@@ -36,6 +36,7 @@ const (
upperUnbounded
unbounded
upperBounded
bounded
)
const maxDepth = 20
@@ -182,6 +183,22 @@ func runSliceBounds(pass *analysis.Pass) (interface{}, error) {
delete(issues, instr)
}
}
case bounded:
switch tinstr := instr.(type) {
case *ssa.Slice:
lower, upper := extractSliceBounds(tinstr)
if isSliceInsideBounds(value, value, lower, upper) {
delete(issues, instr)
}
case *ssa.IndexAddr:
indexValue, err := extractIntValue(tinstr.Index.String())
if err != nil {
break
}
if indexValue == value {
delete(issues, instr)
}
}
}
} else if nestedIfInstr, ok := instr.(*ssa.If); ok {
for _, nestedBlock := range nestedIfInstr.Block().Succs {
@@ -258,23 +275,24 @@ func trackSliceBounds(depth int, sliceCap int, slice ssa.Node, violations *[]ssa
func extractIntValueIndexAddr(refinstr *ssa.IndexAddr, sliceCap int) (int, error) {
var indexIncr, sliceIncr int
idxRefs := refinstr.Index.Referrers()
if idxRefs == nil {
return 0, errors.New("no found")
}
for _, instr := range *idxRefs {
switch instr := instr.(type) {
case *ssa.BinOp:
_, index, err := extractBinOpBound(instr)
if err != nil {
return 0, err
}
switch instr.Op {
case token.LSS:
indexIncr--
}
for _, block := range refinstr.Block().Preds {
for _, instr := range block.Instrs {
switch instr := instr.(type) {
case *ssa.BinOp:
_, index, err := extractBinOpBound(instr)
if err != nil {
return 0, err
}
switch instr.Op {
case token.LSS:
indexIncr--
}
if !isSliceIndexInsideBounds(sliceCap+sliceIncr, index+indexIncr) {
return index, nil
}
if !isSliceIndexInsideBounds(sliceCap+sliceIncr, index+indexIncr) {
return index, nil
}
}
}
@@ -322,19 +340,28 @@ func checkAllSlicesBounds(depth int, sliceCap int, slice *ssa.Slice, violations
func extractSliceIfLenCondition(call *ssa.Call) (*ssa.If, *ssa.BinOp) {
if builtInLen, ok := call.Call.Value.(*ssa.Builtin); ok {
if builtInLen.Name() == "len" {
refs := call.Referrers()
if refs != nil {
for _, ref := range *refs {
refs := []ssa.Instruction{}
if call.Referrers() != nil {
refs = append(refs, *call.Referrers()...)
}
depth := 0
for len(refs) > 0 && depth < maxDepth {
newrefs := []ssa.Instruction{}
for _, ref := range refs {
if binop, ok := ref.(*ssa.BinOp); ok {
binoprefs := binop.Referrers()
for _, ref := range *binoprefs {
if ifref, ok := ref.(*ssa.If); ok {
return ifref, binop
}
newrefs = append(newrefs, ref)
}
}
}
refs = newrefs
depth++
}
}
}
return nil, nil
@@ -363,6 +390,8 @@ func invBound(bound bound) bound {
return unbounded
case unbounded:
return upperBounded
case bounded:
return bounded
default:
return unbounded
}
@@ -386,7 +415,7 @@ func extractBinOpBound(binop *ssa.BinOp) (bound, int, error) {
case token.GTR, token.GEQ:
return lowerUnbounded, value, nil
case token.EQL:
return upperBounded, value, nil
return bounded, value, nil
case token.NEQ:
return unbounded, value, nil
}
@@ -407,7 +436,7 @@ func extractBinOpBound(binop *ssa.BinOp) (bound, int, error) {
case token.GTR, token.GEQ:
return upperUnbounded, value, nil
case token.EQL:
return upperBounded, value, nil
return bounded, value, nil
case token.NEQ:
return unbounded, value, nil
}

View File

@@ -21,7 +21,7 @@ var _ GenAIClient = (*openaiWrapper)(nil)
type OpenAIConfig struct {
Model string
APIKey string
APIKey string `json:"-"`
BaseURL string
MaxTokens int
Temperature float64

View File

@@ -118,6 +118,11 @@ var idWeaknesses = map[string]*Weakness{
Description: "The software does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.",
Name: "Improper Handling of Highly Compressed Data (Data Amplification)",
},
"499": {
ID: "499",
Description: "The code contains a class with sensitive data, but the class does not explicitly deny serialization. The data can be accessed by serializing the class through another class.",
Name: "Serializable Class Containing Sensitive Data",
},
"676": {
ID: "676",
Description: "The program invokes a potentially dangerous function that could introduce a vulnerability if it is used incorrectly, but the function can also be used safely.",

22
go.mod
View File

@@ -7,16 +7,16 @@ require (
github.com/gookit/color v1.6.0
github.com/lib/pq v1.10.9
github.com/mozilla/tls-observatory v0.0.0-20250923143331-eef96233227e
github.com/onsi/ginkgo/v2 v2.27.2
github.com/onsi/gomega v1.38.2
github.com/openai/openai-go/v3 v3.8.1
github.com/onsi/ginkgo/v2 v2.27.3
github.com/onsi/gomega v1.38.3
github.com/openai/openai-go/v3 v3.15.0
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
github.com/stretchr/testify v1.11.1
go.yaml.in/yaml/v3 v3.0.4
golang.org/x/crypto v0.45.0
golang.org/x/text v0.31.0
golang.org/x/tools v0.39.0
google.golang.org/genai v1.37.0
golang.org/x/crypto v0.46.0
golang.org/x/text v0.32.0
golang.org/x/tools v0.40.0
google.golang.org/genai v1.40.0
)
require (
@@ -47,10 +47,10 @@ require (
go.opentelemetry.io/otel v1.37.0 // indirect
go.opentelemetry.io/otel/metric v1.37.0 // indirect
go.opentelemetry.io/otel/trace v1.37.0 // indirect
golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.18.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.39.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250818200422-3122310a409c // indirect
google.golang.org/grpc v1.75.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect

48
go.sum
View File

@@ -306,13 +306,13 @@ github.com/olekukonko/tablewriter v0.0.1/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXW
github.com/olekukonko/tablewriter v0.0.2/go.mod h1:rSAaSIOAGT9odnlyGlUfAJaoc5w2fSBUmeGDbRWPxyQ=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.10.3/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns=
github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/ginkgo/v2 v2.27.3 h1:ICsZJ8JoYafeXFFlFAG75a7CxMsJHwgKwtO+82SE9L8=
github.com/onsi/ginkgo/v2 v2.27.3/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
github.com/openai/openai-go/v3 v3.8.1 h1:b+YWsmwqXnbpSHWQEntZAkKciBZ5CJXwL68j+l59UDg=
github.com/openai/openai-go/v3 v3.8.1/go.mod h1:UOpNxkqC9OdNXNUfpNByKOtB4jAL0EssQXq5p8gO0Xs=
github.com/onsi/gomega v1.38.3 h1:eTX+W6dobAYfFeGC2PV6RwXRu/MyT+cQguijutvkpSM=
github.com/onsi/gomega v1.38.3/go.mod h1:ZCU1pkQcXDO5Sl9/VVEGlDyp+zm0m1cmeG5TOzLgdh4=
github.com/openai/openai-go/v3 v3.15.0 h1:hk99rM7YPz+M99/5B/zOQcVwFRLLMdprVGx1vaZ8XMo=
github.com/openai/openai-go/v3 v3.15.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o=
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU=
@@ -443,8 +443,8 @@ golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
@@ -478,8 +478,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=
golang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -512,8 +512,8 @@ golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/
golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -528,8 +528,8 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
golang.org/x/sync v0.18.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -570,18 +570,18 @@ golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=
golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@@ -633,8 +633,8 @@ golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roY
golang.org/x/tools v0.0.0-20200626171337-aa94e735be7f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200630154851-b2d8b0336632/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20200706234117-b22de6825cf7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=
golang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@@ -663,8 +663,8 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww
google.golang.org/appengine v1.6.2/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/genai v1.37.0 h1:dgp71k1wQ+/+APdZrN3LFgAGnVnr5IdTF1Oj0Dg+BQc=
google.golang.org/genai v1.37.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genai v1.40.0 h1:kYxyQSH+vsib8dvsgyLJzsVEIv5k3ZmHJyVqdvGncmc=
google.golang.org/genai v1.40.0/go.mod h1:A3kkl0nyBjyFlNjgxIwKq70julKbIxpSxqKO5gw/gmk=
google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181107211654-5fc9ac540362/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

View File

@@ -68,6 +68,7 @@ var ruleToCWE = map[string]string{
"G114": "676",
"G115": "190",
"G116": "838",
"G117": "499",
"G201": "89",
"G202": "89",
"G203": "79",

View File

@@ -90,6 +90,12 @@ func TryResolve(n ast.Node, c *Context) bool {
return resolveCallExpr(node, c)
case *ast.BinaryExpr:
return resolveBinExpr(node, c)
case *ast.KeyValueExpr:
return TryResolve(node.Key, c) && TryResolve(node.Value, c)
case *ast.IndexExpr:
return TryResolve(node.X, c)
case *ast.SliceExpr:
return TryResolve(node.X, c)
}
return false
}

View File

@@ -89,6 +89,7 @@ func NewNoErrorCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
whitelist.Add("io.PipeWriter", "CloseWithError")
whitelist.Add("hash.Hash", "Write")
whitelist.Add("os", "Unsetenv")
whitelist.Add("rand", "Read")
if configured, ok := conf[id]; ok {
if whitelisted, ok := configured.(map[string]interface{}); ok {

View File

@@ -229,6 +229,8 @@ func (r *credentials) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error
return r.matchValueSpec(node, ctx)
case *ast.BinaryExpr:
return r.matchEqualityCheck(node, ctx)
case *ast.CompositeLit:
return r.matchCompositeLit(node, ctx)
}
return nil, nil
}
@@ -334,6 +336,44 @@ func (r *credentials) matchEqualityCheck(binaryExpr *ast.BinaryExpr, ctx *gosec.
return nil, nil
}
func (r *credentials) matchCompositeLit(lit *ast.CompositeLit, ctx *gosec.Context) (*issue.Issue, error) {
for _, elt := range lit.Elts {
if kv, ok := elt.(*ast.KeyValueExpr); ok {
// Check if the key matches the credential pattern (struct field name or map string literal key)
matchedKey := false
if ident, ok := kv.Key.(*ast.Ident); ok {
if r.pattern.MatchString(ident.Name) {
matchedKey = true
}
}
if keyStr, err := gosec.GetString(kv.Key); err == nil {
if r.pattern.MatchString(keyStr) {
matchedKey = true
}
}
// If key matches, check value for high entropy (generic credential warning)
if matchedKey {
if val, err := gosec.GetString(kv.Value); err == nil {
if r.ignoreEntropy || r.isHighEntropyString(val) {
return ctx.NewIssue(lit, r.ID(), r.What, r.Severity, r.Confidence), nil
}
}
}
// Separately check value for specific secret patterns (regardless of key)
if val, err := gosec.GetString(kv.Value); err == nil {
if r.ignoreEntropy || r.isHighEntropyString(val) {
if ok, patternName := r.isSecretPattern(val); ok {
return ctx.NewIssue(lit, r.ID(), fmt.Sprintf("%s: %s", r.What, patternName), r.Severity, r.Confidence), nil
}
}
}
}
}
return nil, nil
}
// NewHardcodedCredentials attempts to find high entropy string constants being
// assigned to variables that appear to be related to credentials.
func NewHardcodedCredentials(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
@@ -390,5 +430,5 @@ func NewHardcodedCredentials(id string, conf gosec.Config) (gosec.Rule, []ast.No
Confidence: issue.Low,
Severity: issue.High,
},
}, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ValueSpec)(nil), (*ast.BinaryExpr)(nil)}
}, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ValueSpec)(nil), (*ast.BinaryExpr)(nil), (*ast.CompositeLit)(nil)}
}

View File

@@ -77,6 +77,7 @@ func Generate(trackSuppressions bool, filters ...RuleFilter) RuleList {
{"G112", "Detect ReadHeaderTimeout not configured as a potential risk", NewSlowloris},
{"G114", "Use of net/http serve function that has no support for setting timeouts", NewHTTPServeWithoutTimeouts},
{"G116", "Detect Trojan Source attacks using bidirectional Unicode characters", NewTrojanSource},
{"G117", "Potential exposure of secrets via JSON marshaling", NewSecretSerialization},
// injection
{"G201", "SQL query construction using format string", NewSQLStrFormat},

View File

@@ -111,6 +111,10 @@ var _ = Describe("gosec rules", func() {
runner("G116", testutils.SampleCodeG116)
})
It("should detect exported struct fields that may contain secrets and are JSON serializable", func() {
runner("G117", testutils.SampleCodeG117)
})
It("should detect sql injection via format strings", func() {
runner("G201", testutils.SampleCodeG201)
})

View File

@@ -0,0 +1,123 @@
package rules
import (
"fmt"
"go/ast"
"reflect"
"regexp"
"strconv"
"strings"
"github.com/securego/gosec/v2"
"github.com/securego/gosec/v2/issue"
)
type secretSerialization struct {
issue.MetaData
pattern *regexp.Regexp
}
func (r *secretSerialization) ID() string {
return r.MetaData.ID
}
func (r *secretSerialization) Match(n ast.Node, ctx *gosec.Context) (*issue.Issue, error) {
field, ok := n.(*ast.Field)
if !ok || len(field.Names) == 0 {
return nil, nil // skip embedded (anonymous) fields
}
// Parse the JSON tag to determine behavior
omitted := false
jsonKey := ""
if field.Tag != nil {
if tagVal, err := strconv.Unquote(field.Tag.Value); err == nil && tagVal != "" {
st := reflect.StructTag(tagVal)
if tag := st.Get("json"); tag != "" {
if tag == "-" {
omitted = true
} else {
// "name,omitempty" -> "name"
// "-," -> "-" (A field literally named "-")
parts := strings.SplitN(tag, ",", 2)
jsonKey = parts[0]
}
}
}
}
if omitted {
return nil, nil
}
// Iterate over all names in this field definition
// e.g., type T struct { Pass, Salt string }
isSensitiveType := false
switch t := field.Type.(type) {
case *ast.Ident:
if t.Name == "string" {
isSensitiveType = true
}
case *ast.StarExpr:
if ident, ok := t.X.(*ast.Ident); ok && ident.Name == "string" {
isSensitiveType = true
}
case *ast.ArrayType:
if star, ok := t.Elt.(*ast.StarExpr); ok {
if ident, ok := star.X.(*ast.Ident); ok && ident.Name == "string" {
isSensitiveType = true // []*string
}
} else if ident, ok := t.Elt.(*ast.Ident); ok {
if ident.Name == "string" || ident.Name == "byte" {
isSensitiveType = true // []string or []byte
}
}
}
if !isSensitiveType {
return nil, nil
}
// Check each named exported field
for _, nameIdent := range field.Names {
fieldName := nameIdent.Name
if fieldName == "_" || !ast.IsExported(fieldName) {
continue
}
effectiveKey := jsonKey
if effectiveKey == "" {
effectiveKey = fieldName
}
if r.pattern.MatchString(fieldName) || r.pattern.MatchString(effectiveKey) {
msg := fmt.Sprintf("Exported struct field %q (JSON key %q) matches secret pattern", fieldName, effectiveKey)
return ctx.NewIssue(field, r.ID(), msg, r.Severity, r.Confidence), nil
}
}
return nil, nil
}
func NewSecretSerialization(id string, conf gosec.Config) (gosec.Rule, []ast.Node) {
patternStr := `(?i)\b((?:api|access|auth|bearer|client|oauth|private|refresh|session|jwt)[_-]?(?:key|secret|token)s?|password|passwd|pwd|pass|secret|cred|jwt)\b`
if val, ok := conf[id]; ok {
if m, ok := val.(map[string]interface{}); ok {
if p, ok := m["pattern"].(string); ok && p != "" {
patternStr = p
}
}
}
return &secretSerialization{
pattern: regexp.MustCompile(patternStr),
MetaData: issue.MetaData{
ID: id,
What: "Exported struct field appears to be a secret and is not ignored by JSON marshaling",
Severity: issue.Medium,
Confidence: issue.Medium,
},
}, []ast.Node{(*ast.Field)(nil)}
}

View File

@@ -364,7 +364,123 @@ const (
func main() {
fmt.Printf("%s\n", ConfigLearnerTokenAuth)
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
type DBConfig struct {
Password string
}
func main() {
_ = DBConfig{
Password: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type DBConfig struct {
Password string
}
func main() {
_ = &DBConfig{
Password: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = struct{ Password string }{
Password: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = map[string]string{
"password": "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = map[string]string{
"apiKey": "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
Username string
Password string
}
func main() {
_ = Config{
Username: "admin",
Password: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type DBConfig struct {
Password string
}
func main() {
_ = DBConfig{ // #nosec G101
Password: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef",
}
}
`}, 0, gosec.NewConfig()},
// Negatives
{[]string{`
package main
func main() {
_ = struct{ Password string }{
Password: "secret", // low entropy
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = map[string]string{
"password": "secret", // low entropy
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = struct{ Username string }{
Username: "f62e5bcda4fae4f82370da0c6f20697b8f8447ef", // non-sensitive key
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = []string{"f62e5bcda4fae4f82370da0c6f20697b8f8447ef"} // unkeyed no trigger
}
`}, 0, gosec.NewConfig()},
}
@@ -430,6 +546,24 @@ func main() {
if compareGoogleAPI == "AIzajtGS_aJGkoiAmSbXzu9I-1eytAi9Lrlh-vT" {
fmt.Println(compareGoogleAPI)
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = struct{ SomeKey string }{
SomeKey: "AKIAI44QH8DHBEXAMPLE",
}
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
_ = map[string]string{
"github_token": "ghp_iR54dhCYg9Tfmoywi9xLmmKZrrnAw438BYh3",
}
}
`}, 1, gosec.NewConfig()},
}

View File

@@ -142,6 +142,17 @@ func main() {
b := createBuffer()
b.WriteString("*bytes.Buffer")
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "crypto/rand"
func main() {
b := make([]byte, 8)
rand.Read(b)
_ = b
}
`}, 0, gosec.NewConfig()},
} // it shouldn't return any errors because all method calls are whitelisted by default

197
testutils/g117_samples.go Normal file
View File

@@ -0,0 +1,197 @@
// testutils/g117_samples.go
package testutils
import "github.com/securego/gosec/v2"
var SampleCodeG117 = []CodeSample{
// Positive: match on field name (default JSON key)
{[]string{`
package main
type Config struct {
Password string
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
APIKey *string ` + "`json:\"api_key\"`" + `
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
PrivateKey []byte ` + "`json:\"private_key\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: match on field name (explicit non-sensitive JSON key)
{[]string{`
package main
type Config struct {
Password string ` + "`json:\"text_field\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: match on JSON key (non-sensitive field name)
{[]string{`
package main
type Config struct {
SafeField string ` + "`json:\"api_key\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: match on both
{[]string{`
package main
type Config struct {
Token string ` + "`json:\"auth_token\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: snake/hyphen variants in JSON key
{[]string{`
package main
type Config struct {
Key string ` + "`json:\"access-key\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: empty json tag part falls back to field name
{[]string{`
package main
type Config struct {
Secret string ` + "`json:\",omitempty\"`" + `
}
`}, 1, gosec.NewConfig()},
// Positive: plural forms
{[]string{`
package main
type Config struct {
ApiTokens []string
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
RefreshTokens []string ` + "`json:\"refresh_tokens\"`" + `
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
AccessTokens []*string
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
type Config struct {
CustomSecret string ` + "`json:\"my_custom_secret\"`" + `
}
`}, 1, func() gosec.Config {
cfg := gosec.NewConfig()
cfg.Set("G117", map[string]interface{}{
"pattern": "(?i)custom[_-]?secret",
})
return cfg
}()},
// Negative: json:"-" (omitted)
{[]string{`
package main
type Config struct {
Password string ` + "`json:\"-\"`" + `
}
`}, 0, gosec.NewConfig()},
// Negative: both field name and JSON key non-sensitive
{[]string{`
package main
type Config struct {
UserID string ` + "`json:\"user_id\"`" + `
}
`}, 0, gosec.NewConfig()},
// Negative: unexported field
{[]string{`
package main
type Config struct {
password string
}
`}, 0, gosec.NewConfig()},
// Negative: non-sensitive type (int) even with "token"
{[]string{`
package main
type Config struct {
MaxTokens int
}
`}, 0, gosec.NewConfig()},
// Negative: non-secret plural slice (common FP like redaction placeholders)
{[]string{`
package main
type Config struct {
RedactionTokens []string ` + "`json:\"redactionTokens,omitempty\"`" + `
}
`}, 0, gosec.NewConfig()},
// Negative: grouped fields, only one sensitive (should still flag the sensitive one)
// Note: we expect 1 issue (for the sensitive field)
{[]string{`
package main
type Config struct {
Safe, Password string
}
`}, 1, gosec.NewConfig()},
// Suppression: trailing line comment
{[]string{`
package main
type Config struct {
Password string // #nosec G117
}
`}, 0, gosec.NewConfig()},
// Suppression: line comment above field
{[]string{`
package main
type Config struct {
// #nosec G117 -- false positive
Password string
}
`}, 0, gosec.NewConfig()},
// Suppression: trailing with justification
{[]string{`
package main
type Config struct {
APIKey string ` + "`json:\"api_key\"`" + ` // #nosec G117 -- public key
}
`}, 0, gosec.NewConfig()},
}

View File

@@ -242,4 +242,40 @@ func main() {
log.Printf("Command finished with error: %v", err)
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
import (
"os/exec"
"runtime"
)
// Safe OS-specific command selection using a hard-coded map and slice operations.
// Closely matches the pattern in https://github.com/securego/gosec/issues/1199.
// The command name and fixed arguments are fully resolved from constant composite literals,
// even though the map key is runtime.GOOS (non-constant in analysis).
func main() {
commands := map[string][]string{
"darwin": {"open"},
"freebsd": {"xdg-open"},
"linux": {"xdg-open"},
"netbsd": {"xdg-open"},
"openbsd": {"xdg-open"},
"windows": {"cmd", "/c", "start"},
}
platform := runtime.GOOS
cmdArgs := commands[platform]
if cmdArgs == nil {
return // unsupported platform
}
exe := cmdArgs[0]
args := cmdArgs[1:]
// No dynamic/tainted input; fixed args passed via ... expansion
_ = exec.Command(exe, args...)
}
`}, 0, gosec.NewConfig()},
}

View File

@@ -413,4 +413,39 @@ func main() {
}
`}, 1, gosec.NewConfig()},
{[]string{`
package main
func main() {
args := []any{"1"}
switch len(args) - 1 {
case 1:
_ = args[1]
}
}
`}, 0, gosec.NewConfig()},
{[]string{`
package main
import "fmt"
func main() {
value := "1234567890"
weight := []int{2, 3, 4, 5, 6, 7}
wLen := len(weight)
l := len(value) - 1
addr := make([]any, 7)
sum := 0
weight[2] = 3
for i := l; i >= 0; i-- {
v := int(value[i] - '0')
if v < 0 || v > 9 {
fmt.Println("invalid number at column", i+1)
break
}
addr[2] = v
sum += v * weight[(l-i)%wLen]
}
fmt.Println(sum)
}
`}, 0, gosec.NewConfig()},
}