├── .github ├── FUNDING.yml ├── issue_template.md └── workflows │ ├── build-image.yaml │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yml ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── USERS.md ├── action.yml ├── analyzer.go ├── analyzer_skip_generated_go_test.go ├── analyzer_test.go ├── call_list.go ├── call_list_test.go ├── cmd ├── gosec │ ├── main.go │ ├── sort_issues.go │ ├── sort_issues_test.go │ └── version.go ├── gosecutil │ └── tools.go └── tlsconfig │ ├── header_template.go │ ├── rule_template.go │ ├── tls_version_go12_go13.go │ ├── tls_version_go14.go │ └── tlsconfig.go ├── config.go ├── config_test.go ├── entrypoint.sh ├── errors.go ├── go.mod ├── go.sum ├── gosec_suite_test.go ├── helpers.go ├── helpers_test.go ├── import_tracker.go ├── import_tracker_test.go ├── install.sh ├── issue.go ├── issue_test.go ├── output ├── formatter.go ├── formatter_suite_test.go ├── formatter_test.go ├── junit_xml_format.go ├── sarif_format.go ├── sonarqube_format.go └── template.go ├── renovate.json ├── resolve.go ├── resolve_test.go ├── rule.go ├── rule_test.go ├── rules ├── archive.go ├── bad_defer.go ├── bind.go ├── blocklist.go ├── decompression-bomb.go ├── errors.go ├── fileperms.go ├── hardcoded_credentials.go ├── implicit_aliasing.go ├── integer_overflow.go ├── pprof.go ├── rand.go ├── readfile.go ├── rsa.go ├── rulelist.go ├── rules_suite_test.go ├── rules_test.go ├── sdk │ ├── README.md │ ├── blocklist.go │ ├── errors.go │ ├── int_end_conversion_test.go │ ├── integer.go │ ├── iterate_over_maps.go │ └── strconv_bitsize_mismatch.go ├── sql.go ├── ssh.go ├── ssrf.go ├── subproc.go ├── tempfiles.go ├── templates.go ├── tls.go ├── tls_config.go ├── unsafe.go └── weakcrypto.go ├── testdata ├── with_cgo_import_generated_code.go ├── with_cgo_import_no_generated_code.go ├── with_generated_header.go ├── with_regular_code_comment_about_generated.go └── without_generated_header.go └── testutils ├── log.go ├── pkg.go ├── source.go └── visitor.go /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ccojocar, gcmurphy] 4 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ### Summary 2 | 3 | ### Steps to reproduce the behavior 4 | 5 | ### gosec version 6 | 7 | ### Go version (output of 'go version') 8 | 9 | ### Operating system / Environment 10 | 11 | ### Expected behavior 12 | 13 | ### Actual behavior 14 | -------------------------------------------------------------------------------- /.github/workflows/build-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build Docker image 2 | 3 | on: 4 | pull_request: 5 | 6 | env: 7 | GO_VERSION: 1.17 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Build Docker image 17 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 18 | with: 19 | context: . 20 | build-args: GO_VERSION=${{ env.GO_VERSION }} 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: 8 | - master 9 | jobs: 10 | tests: 11 | strategy: 12 | matrix: 13 | go_version: 14 | - '1.17' 15 | runs-on: ubuntu-latest 16 | env: 17 | GO111MODULE: on 18 | steps: 19 | - name: Setup go ${{ matrix.go_version }} 20 | uses: actions/setup-go@v3 21 | with: 22 | go-version: ${{ matrix.go_version }} 23 | - name: Checkout Source 24 | uses: actions/checkout@v2 25 | - uses: actions/cache@v3 26 | with: 27 | path: ~/go/pkg/mod 28 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 29 | restore-keys: | 30 | ${{ runner.os }}-go- 31 | - name: Run Tests 32 | run: make test 33 | coverage: 34 | needs: tests 35 | runs-on: ubuntu-latest 36 | env: 37 | GO111MODULE: on 38 | steps: 39 | - name: Setup go 40 | uses: actions/setup-go@v3 41 | with: 42 | go-version: '1.18' 43 | - name: Checkout Source 44 | uses: actions/checkout@v2 45 | - uses: actions/cache@v3 46 | with: 47 | path: ~/go/pkg/mod 48 | key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} 49 | restore-keys: | 50 | ${{ runner.os }}-go- 51 | - name: Create Test Coverage 52 | run: make test-coverage 53 | - name: Upload Test Coverage 54 | uses: codecov/codecov-action@v1 55 | with: 56 | token: ${{ secrets.CODECOV_TOKEN }} 57 | file: ./coverage.txt 58 | fail_ci_if_error: true 59 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | GO_VERSION: 1.17 11 | 12 | jobs: 13 | build-and-push-image: 14 | runs-on: ubuntu-latest 15 | permissions: 16 | contents: read 17 | packages: write 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Log in to the Container registry 24 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 25 | with: 26 | registry: ${{ env.REGISTRY }} 27 | username: ${{ github.actor }} 28 | password: ${{ secrets.GITHUB_TOKEN }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 33 | with: 34 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 35 | 36 | - name: Build and push Docker image 37 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 38 | with: 39 | context: . 40 | push: true 41 | tags: ${{ steps.meta.outputs.tags }} 42 | labels: ${{ steps.meta.outputs.labels }} 43 | build-args: GO_VERSION=${{ env.GO_VERSION }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # transient files 2 | /image 3 | 4 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 5 | *.o 6 | *.a 7 | *.so 8 | *.swp 9 | /gosec 10 | 11 | # Folders 12 | _obj 13 | _test 14 | vendor 15 | dist 16 | 17 | # Architecture specific extensions/prefixes 18 | *.[568vq] 19 | [568vq].out 20 | 21 | *.cgo1.go 22 | *.cgo2.c 23 | _cgo_defun.c 24 | _cgo_gotypes.go 25 | _cgo_export.* 26 | 27 | _testmain.go 28 | 29 | *.exe 30 | *.test 31 | *.prof 32 | 33 | .DS_Store 34 | 35 | .vscode 36 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | --- 2 | project_name: gosec 3 | 4 | release: 5 | github: 6 | owner: cosmos 7 | name: gosec 8 | 9 | builds: 10 | - main : ./cmd/gosec/ 11 | binary: gosec 12 | goos: 13 | - darwin 14 | - linux 15 | - windows 16 | goarch: 17 | - amd64 18 | ldflags: -X main.Version={{.Version}} -X main.GitTag={{.Tag}} -X main.BuildDate={{.Date}} 19 | env: 20 | - CGO_ENABLED=0 21 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG GO_VERSION 2 | FROM golang:${GO_VERSION}-alpine AS builder 3 | RUN apk add --update --no-cache ca-certificates make git curl gcc libc-dev 4 | RUN mkdir -p /build 5 | WORKDIR /build 6 | COPY . /build/ 7 | RUN go mod download 8 | RUN make build-linux 9 | 10 | FROM golang:${GO_VERSION}-alpine 11 | RUN apk add --update --no-cache ca-certificates bash git gcc libc-dev 12 | ENV GO111MODULE on 13 | COPY --from=builder /build/gosec /bin/gosec 14 | COPY entrypoint.sh /bin/entrypoint.sh 15 | ENTRYPOINT ["/bin/entrypoint.sh"] 16 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | 3 | Version 2.0, January 2004 4 | 5 | http://www.apache.org/licenses/ 6 | 7 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 8 | 9 | 1. Definitions. 10 | 11 | "License" shall mean the terms and conditions for use, reproduction, and 12 | distribution as defined by Sections 1 through 9 of this document. 13 | 14 | "Licensor" shall mean the copyright owner or entity authorized by the copyright 15 | owner that is granting the License. 16 | 17 | "Legal Entity" shall mean the union of the acting entity and all other entities 18 | that control, are controlled by, or are under common control with that entity. 19 | For the purposes of this definition, "control" means (i) the power, direct or 20 | indirect, to cause the direction or management of such entity, whether by 21 | contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity exercising 25 | permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, including 28 | but not limited to software source code, documentation source, and configuration 29 | files. 30 | 31 | "Object" form shall mean any form resulting from mechanical transformation or 32 | translation of a Source form, including but not limited to compiled object code, 33 | generated documentation, and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or Object form, made 36 | available under the License, as indicated by a copyright notice that is included 37 | in or attached to the work (an example is provided in the Appendix below). 38 | 39 | "Derivative Works" shall mean any work, whether in Source or Object form, that 40 | is based on (or derived from) the Work and for which the editorial revisions, 41 | annotations, elaborations, or other modifications represent, as a whole, an 42 | original work of authorship. For the purposes of this License, Derivative Works 43 | shall not include works that remain separable from, or merely link (or bind by 44 | name) to the interfaces of, the Work and Derivative Works thereof. 45 | 46 | "Contribution" shall mean any work of authorship, including the original version 47 | of the Work and any modifications or additions to that Work or Derivative Works 48 | thereof, that is intentionally submitted to Licensor for inclusion in the Work 49 | by the copyright owner or by an individual or Legal Entity authorized to submit 50 | on behalf of the copyright owner. For the purposes of this definition, 51 | "submitted" means any form of electronic, verbal, or written communication sent 52 | to the Licensor or its representatives, including but not limited to 53 | communication on electronic mailing lists, source code control systems, and 54 | issue tracking systems that are managed by, or on behalf of, the Licensor for 55 | the purpose of discussing and improving the Work, but excluding communication 56 | that is conspicuously marked or otherwise designated in writing by the copyright 57 | owner as "Not a Contribution." 58 | 59 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf 60 | of whom a Contribution has been received by Licensor and subsequently 61 | incorporated within the Work. 62 | 63 | 2. Grant of Copyright License. Subject to the terms and conditions of this 64 | License, each Contributor hereby grants to You a perpetual, worldwide, 65 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 66 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 67 | sublicense, and distribute the Work and such Derivative Works in Source or 68 | Object form. 69 | 70 | 3. Grant of Patent License. Subject to the terms and conditions of this License, 71 | each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, 72 | no-charge, royalty-free, irrevocable (except as stated in this section) patent 73 | license to make, have made, use, offer to sell, sell, import, and otherwise 74 | transfer the Work, where such license applies only to those patent claims 75 | licensable by such Contributor that are necessarily infringed by their 76 | Contribution(s) alone or by combination of their Contribution(s) with the Work 77 | to which such Contribution(s) was submitted. If You institute patent litigation 78 | against any entity (including a cross-claim or counterclaim in a lawsuit) 79 | alleging that the Work or a Contribution incorporated within the Work 80 | constitutes direct or contributory patent infringement, then any patent licenses 81 | granted to You under this License for that Work shall terminate as of the date 82 | such litigation is filed. 83 | 84 | 4. Redistribution. You may reproduce and distribute copies of the Work or 85 | Derivative Works thereof in any medium, with or without modifications, and in 86 | Source or Object form, provided that You meet the following conditions: 87 | 88 | You must give any other recipients of the Work or Derivative Works a copy of 89 | this License; and You must cause any modified files to carry prominent notices 90 | stating that You changed the files; and You must retain, in the Source form of 91 | any Derivative Works that You distribute, all copyright, patent, trademark, and 92 | attribution notices from the Source form of the Work, excluding those notices 93 | that do not pertain to any part of the Derivative Works; and If the Work 94 | includes a "NOTICE" text file as part of its distribution, then any Derivative 95 | Works that You distribute must include a readable copy of the attribution 96 | notices contained within such NOTICE file, excluding those notices that do not 97 | pertain to any part of the Derivative Works, in at least one of the following 98 | places: within a NOTICE text file distributed as part of the Derivative Works; 99 | within the Source form or documentation, if provided along with the Derivative 100 | Works; or, within a display generated by the Derivative Works, if and wherever 101 | such third-party notices normally appear. The contents of the NOTICE file are 102 | for informational purposes only and do not modify the License. You may add Your 103 | own attribution notices within Derivative Works that You distribute, alongside 104 | or as an addendum to the NOTICE text from the Work, provided that such 105 | additional attribution notices cannot be construed as modifying the License. 106 | 107 | You may add Your own copyright statement to Your modifications and may provide 108 | additional or different license terms and conditions for use, reproduction, or 109 | distribution of Your modifications, or for any such Derivative Works as a whole, 110 | provided Your use, reproduction, and distribution of the Work otherwise complies 111 | with the conditions stated in this License. 5. Submission of Contributions. 112 | Unless You explicitly state otherwise, any Contribution intentionally submitted 113 | for inclusion in the Work by You to the Licensor shall be under the terms and 114 | conditions of this License, without any additional terms or conditions. 115 | Notwithstanding the above, nothing herein shall supersede or modify the terms of 116 | any separate license agreement you may have executed with Licensor regarding 117 | such Contributions. 118 | 119 | 6. Trademarks. This License does not grant permission to use the trade names, 120 | trademarks, service marks, or product names of the Licensor, except as required 121 | for reasonable and customary use in describing the origin of the Work and 122 | reproducing the content of the NOTICE file. 123 | 124 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 125 | writing, Licensor provides the Work (and each Contributor provides its 126 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 127 | KIND, either express or implied, including, without limitation, any warranties 128 | or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 129 | PARTICULAR PURPOSE. You are solely responsible for determining the 130 | appropriateness of using or redistributing the Work and assume any risks 131 | associated with Your exercise of permissions under this License. 132 | 133 | 8. Limitation of Liability. In no event and under no legal theory, whether in 134 | tort (including negligence), contract, or otherwise, unless required by 135 | applicable law (such as deliberate and grossly negligent acts) or agreed to in 136 | writing, shall any Contributor be liable to You for damages, including any 137 | direct, indirect, special, incidental, or consequential damages of any character 138 | arising as a result of this License or out of the use or inability to use the 139 | Work (including but not limited to damages for loss of goodwill, work stoppage, 140 | computer failure or malfunction, or any and all other commercial damages or 141 | losses), even if such Contributor has been advised of the possibility of such 142 | damages. 143 | 144 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or 145 | Derivative Works thereof, You may choose to offer, and charge a fee for, 146 | acceptance of support, warranty, indemnity, or other liability obligations 147 | and/or rights consistent with this License. However, in accepting such 148 | obligations, You may act only on Your own behalf and on Your sole 149 | responsibility, not on behalf of any other Contributor, and only if You agree to 150 | indemnify, defend, and hold each Contributor harmless for any liability incurred 151 | by, or claims asserted against, such Contributor by reason of your accepting any 152 | such warranty or additional liability. 153 | 154 | END OF TERMS AND CONDITIONS 155 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_TAG?= $(shell git describe --always --tags) 2 | BIN = gosec 3 | FMT_CMD = $(gofmt -s -l -w $(find . -type f -name '*.go' -not -path './vendor/*') | tee /dev/stderr) 4 | IMAGE_REPO = cosmos 5 | BUILDFLAGS := '-w -s' 6 | CGO_ENABLED = 0 7 | GO := GO111MODULE=on go 8 | GOPATH ?= $(shell $(GO) env GOPATH) 9 | GOBIN ?= $(GOPATH)/bin 10 | GOLINT ?= $(GOBIN)/golint 11 | GOSEC ?= $(GOBIN)/gosec 12 | GINKGO ?= $(GOBIN)/ginkgo 13 | GO_VERSION = 1.17 14 | 15 | default: 16 | $(MAKE) build 17 | 18 | install-test-deps: 19 | go get -u github.com/onsi/ginkgo/ginkgo 20 | go get -u golang.org/x/crypto/ssh 21 | go get -u github.com/lib/pq 22 | 23 | test: install-test-deps build fmt lint sec 24 | $(GINKGO) -r -v 25 | 26 | fmt: 27 | @echo "FORMATTING" 28 | @FORMATTED=`$(GO) fmt ./...` 29 | @([[ ! -z "$(FORMATTED)" ]] && printf "Fixed unformatted files:\n$(FORMATTED)") || true 30 | 31 | lint: 32 | @echo "LINTING" 33 | go get -u golang.org/x/lint/golint 34 | $(GOLINT) -set_exit_status ./... 35 | @echo "VETTING" 36 | $(GO) vet ./... 37 | 38 | sec: 39 | @echo "SECURITY SCANNING" 40 | ./$(BIN) ./... 41 | 42 | test-coverage: install-test-deps 43 | go test -race -coverprofile=coverage.txt -covermode=atomic 44 | 45 | build: 46 | go build -o $(BIN) ./cmd/gosec/ 47 | 48 | clean: 49 | rm -rf build vendor dist coverage.txt 50 | rm -f release image $(BIN) 51 | 52 | release: 53 | @echo "Releasing the gosec binary..." 54 | goreleaser release 55 | 56 | build-linux: 57 | CGO_ENABLED=$(CGO_ENABLED) GOOS=linux GOARCH=amd64 go build -ldflags $(BUILDFLAGS) -o $(BIN) ./cmd/gosec/ 58 | 59 | image: 60 | @echo "Building the Docker image..." 61 | docker build -t $(IMAGE_REPO)/$(BIN):$(GIT_TAG) --build-arg GO_VERSION=$(GO_VERSION) . 62 | docker tag $(IMAGE_REPO)/$(BIN):$(GIT_TAG) $(IMAGE_REPO)/$(BIN):latest 63 | touch image 64 | 65 | image-push: image 66 | @echo "Pushing the Docker image..." 67 | docker push $(IMAGE_REPO)/$(BIN):$(GIT_TAG) 68 | docker push $(IMAGE_REPO)/$(BIN):latest 69 | 70 | .PHONY: test build clean release image image-push 71 | -------------------------------------------------------------------------------- /USERS.md: -------------------------------------------------------------------------------- 1 | # Users 2 | 3 | This is a list of gosec's users. Please send a pull request with your organisation or project name if you are using gosec. 4 | 5 | ## Companies 6 | 7 | 1. [Gitlab](https://docs.gitlab.com/ee/user/application_security/sast/) 8 | 2. [CloudBees](https://cloudbees.com) 9 | 3. [VMware](https://www.vmware.com) 10 | 4. [Codacy](https://support.codacy.com/hc/en-us/articles/213632009-Engines) 11 | 5. [Coinbase](https://github.com/coinbase/watchdog/blob/master/Makefile#L12) 12 | 6. [RedHat/OpenShift](https://github.com/openshift/openshift-azure) 13 | 7. [Guardalis](https://www.guardrails.io/) 14 | 8. [1Password](https://github.com/1Password/srp) 15 | 9. [PingCAP/tidb](https://github.com/pingcap/tidb) 16 | 17 | ## Projects 18 | 19 | 1. [golangci-lint](https://github.com/golangci/golangci-lint) 20 | 2. [Kubenetes](https://github.com/kubernetes/kubernetes) (via golangci) 21 | 3. [caddy](https://github.com/caddyserver/caddy) (via golangci) 22 | 4. [Jenkins X](https://github.com/jenkins-x/jx/blob/bdc51840a41b75776159c1c7b7faa1cf477be473/hack/linter.sh#L25) 23 | 5. [HuskyCI](https://huskyci.opensource.globo.com/) 24 | 6. [GolangCI](https://golangci.com/) 25 | 7. [semgrep.live](https://semgrep.live/) 26 | 8. [gofiber](https://github.com/gofiber/fiber) 27 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Cosmos Gosec Security Checker' 2 | description: 'Runs the gosec security checker' 3 | author: 'Cosmos Ecosystem' 4 | 5 | inputs: 6 | args: 7 | description: 'Arguments for gosec' 8 | required: true 9 | default: '-h' 10 | 11 | runs: 12 | using: 'docker' 13 | image: 'docker://ghcr.io/cosmos/gosec' 14 | args: 15 | - ${{ inputs.args }} 16 | 17 | branding: 18 | icon: 'shield' 19 | color: 'blue' 20 | -------------------------------------------------------------------------------- /analyzer_skip_generated_go_test.go: -------------------------------------------------------------------------------- 1 | package gosec 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | "sort" 7 | "strings" 8 | "testing" 9 | 10 | "github.com/google/go-cmp/cmp" 11 | ) 12 | 13 | func TestUnitFilterOutGeneratedGoFiles(t *testing.T) { 14 | f, err := os.Open("./testdata") 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | defer f.Close() 19 | 20 | fiL, err := f.Readdir(-1) 21 | if err != nil { 22 | t.Fatal(err) 23 | } 24 | 25 | goFiles := make([]string, 0, 10) 26 | for _, fi := range fiL { 27 | if !fi.IsDir() && strings.HasSuffix(fi.Name(), ".go") { 28 | goFiles = append(goFiles, filepath.Join(f.Name(), fi.Name())) 29 | } 30 | } 31 | 32 | filtered := filterOutGeneratedGoFiles(goFiles) 33 | want := []string{ 34 | "testdata/without_generated_header.go", 35 | "testdata/with_cgo_import_no_generated_code.go", 36 | "testdata/with_regular_code_comment_about_generated.go", 37 | } 38 | sort.Strings(filtered) 39 | sort.Strings(want) 40 | if diff := cmp.Diff(filtered, want); diff != "" { 41 | t.Fatalf("Result mismatch: got - want +\n%s", diff) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /call_list.go: -------------------------------------------------------------------------------- 1 | // 2 | // Licensed under the Apache License, Version 2.0 (the "License"); 3 | // you may not use this file except in compliance with the License. 4 | // You may obtain a copy of the License at 5 | // 6 | // http://www.apache.org/licenses/LICENSE-2.0 7 | // 8 | // Unless required by applicable law or agreed to in writing, software 9 | // distributed under the License is distributed on an "AS IS" BASIS, 10 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 11 | // See the License for the specific language governing permissions and 12 | // limitations under the License. 13 | 14 | package gosec 15 | 16 | import ( 17 | "go/ast" 18 | "strings" 19 | ) 20 | 21 | const vendorPath = "vendor/" 22 | 23 | type set map[string]bool 24 | 25 | // CallList is used to check for usage of specific packages 26 | // and functions. 27 | type CallList map[string]set 28 | 29 | // NewCallList creates a new empty CallList 30 | func NewCallList() CallList { 31 | return make(CallList) 32 | } 33 | 34 | // AddAll will add several calls to the call list at once 35 | func (c CallList) AddAll(selector string, idents ...string) { 36 | for _, ident := range idents { 37 | c.Add(selector, ident) 38 | } 39 | } 40 | 41 | // Add a selector and call to the call list 42 | func (c CallList) Add(selector, ident string) { 43 | if _, ok := c[selector]; !ok { 44 | c[selector] = make(set) 45 | } 46 | c[selector][ident] = true 47 | } 48 | 49 | // Contains returns true if the package and function are 50 | // / members of this call list. 51 | func (c CallList) Contains(selector, ident string) bool { 52 | if idents, ok := c[selector]; ok { 53 | _, found := idents[ident] 54 | return found 55 | } 56 | return false 57 | } 58 | 59 | // ContainsPointer returns true if a pointer to the selector type or the type 60 | // itself is a members of this call list. 61 | func (c CallList) ContainsPointer(selector, indent string) bool { 62 | if strings.HasPrefix(selector, "*") { 63 | if c.Contains(selector, indent) { 64 | return true 65 | } 66 | s := strings.TrimPrefix(selector, "*") 67 | return c.Contains(s, indent) 68 | } 69 | return false 70 | } 71 | 72 | // ContainsPkgCallExpr resolves the call expression name and type, and then further looks 73 | // up the package path for that type. Finally, it determines if the call exists within the call list 74 | func (c CallList) ContainsPkgCallExpr(n ast.Node, ctx *Context, stripVendor bool) *ast.CallExpr { 75 | selector, ident, err := GetCallInfo(n, ctx) 76 | if err != nil { 77 | return nil 78 | } 79 | 80 | // Use only explicit path (optionally strip vendor path prefix) to reduce conflicts 81 | path, ok := GetImportPath(selector, ctx) 82 | if !ok { 83 | return nil 84 | } 85 | if stripVendor { 86 | if vendorIdx := strings.Index(path, vendorPath); vendorIdx >= 0 { 87 | path = path[vendorIdx+len(vendorPath):] 88 | } 89 | } 90 | if !c.Contains(path, ident) { 91 | return nil 92 | } 93 | 94 | return n.(*ast.CallExpr) 95 | } 96 | 97 | // ContainsCallExpr resolves the call expression name and type, and then determines 98 | // if the call exists with the call list 99 | func (c CallList) ContainsCallExpr(n ast.Node, ctx *Context) *ast.CallExpr { 100 | selector, ident, err := GetCallInfo(n, ctx) 101 | if err != nil { 102 | return nil 103 | } 104 | if !c.Contains(selector, ident) && !c.ContainsPointer(selector, ident) { 105 | return nil 106 | } 107 | 108 | return n.(*ast.CallExpr) 109 | } 110 | -------------------------------------------------------------------------------- /call_list_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | "github.com/cosmos/gosec/v2/testutils" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Call List", func() { 13 | var ( 14 | calls gosec.CallList 15 | ) 16 | BeforeEach(func() { 17 | calls = gosec.NewCallList() 18 | }) 19 | 20 | It("should not return any matches when empty", func() { 21 | Expect(calls.Contains("foo", "bar")).Should(BeFalse()) 22 | }) 23 | 24 | It("should be possible to add a single call", func() { 25 | Expect(calls).Should(HaveLen(0)) 26 | calls.Add("foo", "bar") 27 | Expect(calls).Should(HaveLen(1)) 28 | 29 | expected := make(map[string]bool) 30 | expected["bar"] = true 31 | actual := map[string]bool(calls["foo"]) 32 | Expect(actual).Should(Equal(expected)) 33 | }) 34 | 35 | It("should be possible to add multiple calls at once", func() { 36 | Expect(calls).Should(HaveLen(0)) 37 | calls.AddAll("fmt", "Sprint", "Sprintf", "Printf", "Println") 38 | 39 | expected := map[string]bool{ 40 | "Sprint": true, 41 | "Sprintf": true, 42 | "Printf": true, 43 | "Println": true, 44 | } 45 | actual := map[string]bool(calls["fmt"]) 46 | Expect(actual).Should(Equal(expected)) 47 | }) 48 | 49 | It("should be possible to add pointer call", func() { 50 | Expect(calls).Should(HaveLen(0)) 51 | calls.Add("*bytes.Buffer", "WriteString") 52 | actual := calls.ContainsPointer("*bytes.Buffer", "WriteString") 53 | Expect(actual).Should(BeTrue()) 54 | }) 55 | 56 | It("should be possible to check pointer call", func() { 57 | Expect(calls).Should(HaveLen(0)) 58 | calls.Add("bytes.Buffer", "WriteString") 59 | actual := calls.ContainsPointer("*bytes.Buffer", "WriteString") 60 | Expect(actual).Should(BeTrue()) 61 | }) 62 | 63 | It("should not return a match if none are present", func() { 64 | calls.Add("ioutil", "Copy") 65 | Expect(calls.Contains("fmt", "Println")).Should(BeFalse()) 66 | }) 67 | 68 | It("should match a call based on selector and ident", func() { 69 | calls.Add("ioutil", "Copy") 70 | Expect(calls.Contains("ioutil", "Copy")).Should(BeTrue()) 71 | }) 72 | 73 | It("should match a package call expression", func() { 74 | // Create file to be scanned 75 | pkg := testutils.NewTestPackage() 76 | defer pkg.Close() 77 | pkg.AddFile("md5.go", testutils.SampleCodeG401[0].Code[0]) 78 | 79 | ctx := pkg.CreateContext("md5.go") 80 | 81 | // Search for md5.New() 82 | calls.Add("crypto/md5", "New") 83 | 84 | // Stub out visitor and count number of matched call expr 85 | matched := 0 86 | v := testutils.NewMockVisitor() 87 | v.Context = ctx 88 | v.Callback = func(n ast.Node, ctx *gosec.Context) bool { 89 | if _, ok := n.(*ast.CallExpr); ok && calls.ContainsPkgCallExpr(n, ctx, false) != nil { 90 | matched++ 91 | } 92 | return true 93 | } 94 | ast.Walk(v, ctx.Root) 95 | Expect(matched).Should(Equal(1)) 96 | }) 97 | 98 | It("should match a call expression", func() { 99 | // Create file to be scanned 100 | pkg := testutils.NewTestPackage() 101 | defer pkg.Close() 102 | pkg.AddFile("main.go", testutils.SampleCodeG104[6].Code[0]) 103 | 104 | ctx := pkg.CreateContext("main.go") 105 | 106 | calls.Add("bytes.Buffer", "WriteString") 107 | calls.Add("strings.Builder", "WriteString") 108 | calls.Add("io.Pipe", "CloseWithError") 109 | calls.Add("fmt", "Fprintln") 110 | 111 | // Stub out visitor and count number of matched call expr 112 | matched := 0 113 | v := testutils.NewMockVisitor() 114 | v.Context = ctx 115 | v.Callback = func(n ast.Node, ctx *gosec.Context) bool { 116 | if _, ok := n.(*ast.CallExpr); ok && calls.ContainsCallExpr(n, ctx) != nil { 117 | matched++ 118 | } 119 | return true 120 | } 121 | ast.Walk(v, ctx.Root) 122 | Expect(matched).Should(Equal(5)) 123 | }) 124 | }) 125 | -------------------------------------------------------------------------------- /cmd/gosec/sort_issues.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "sort" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/cosmos/gosec/v2" 9 | ) 10 | 11 | // handle ranges 12 | func extractLineNumber(s string) int { 13 | lineNumber, err := strconv.Atoi(strings.Split(s, "-")[0]) 14 | if err != nil { 15 | // TODO: Should this panic? 16 | lineNumber = 0 17 | } 18 | return lineNumber 19 | 20 | } 21 | 22 | type sortBySeverity []*gosec.Issue 23 | 24 | func (s sortBySeverity) Len() int { return len(s) } 25 | 26 | func (s sortBySeverity) Less(i, j int) bool { 27 | if s[i].Severity != s[j].Severity { 28 | return s[i].Severity > s[j].Severity 29 | } 30 | 31 | if s[i].What != s[j].What { 32 | return s[i].What > s[j].What 33 | } 34 | 35 | if s[i].File == s[j].File { 36 | return extractLineNumber(s[i].Line) > extractLineNumber(s[j].Line) 37 | } 38 | return s[i].File > s[j].File 39 | } 40 | 41 | func (s sortBySeverity) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 42 | 43 | // sortIssues sorts the issues by severity in descending order 44 | func sortIssues(issues []*gosec.Issue) { 45 | sort.Sort(sortBySeverity(issues)) 46 | } 47 | -------------------------------------------------------------------------------- /cmd/gosec/sort_issues_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var defaultIssue = gosec.Issue{ 12 | File: "/home/src/project/test.go", 13 | Line: "1", 14 | Col: "1", 15 | RuleID: "ruleID", 16 | What: "test", 17 | Confidence: gosec.High, 18 | Severity: gosec.High, 19 | Code: "1: testcode", 20 | Cwe: gosec.GetCwe("G101"), 21 | } 22 | 23 | func createIssue() gosec.Issue { 24 | return defaultIssue 25 | } 26 | 27 | func TestRules(t *testing.T) { 28 | RegisterFailHandler(Fail) 29 | RunSpecs(t, "Sort issues Suite") 30 | } 31 | 32 | func firstIsGreater(less, greater *gosec.Issue) { 33 | slice := []*gosec.Issue{less, greater} 34 | 35 | sortIssues(slice) 36 | 37 | ExpectWithOffset(0, slice[0]).To(Equal(greater)) 38 | } 39 | 40 | var _ = Describe("Sorting by Severity", func() { 41 | It("sortes by severity", func() { 42 | less := createIssue() 43 | less.Severity = gosec.Low 44 | greater := createIssue() 45 | less.Severity = gosec.High 46 | firstIsGreater(&less, &greater) 47 | }) 48 | 49 | Context("Serverity is same", func() { 50 | It("sortes by What", func() { 51 | less := createIssue() 52 | less.What = "test1" 53 | greater := createIssue() 54 | greater.What = "test2" 55 | firstIsGreater(&less, &greater) 56 | }) 57 | }) 58 | 59 | Context("Serverity and What is same", func() { 60 | It("sortes by File", func() { 61 | less := createIssue() 62 | less.File = "test1" 63 | greater := createIssue() 64 | greater.File = "test2" 65 | 66 | firstIsGreater(&less, &greater) 67 | }) 68 | }) 69 | 70 | Context("Serverity, What and File is same", func() { 71 | It("sortes by line number", func() { 72 | less := createIssue() 73 | less.Line = "1" 74 | greater := createIssue() 75 | greater.Line = "2" 76 | 77 | firstIsGreater(&less, &greater) 78 | }) 79 | }) 80 | }) 81 | -------------------------------------------------------------------------------- /cmd/gosec/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | // Version is the build version 4 | var Version string 5 | 6 | // GitTag is the git tag of the build 7 | var GitTag string 8 | 9 | // BuildDate is the date when the build was created 10 | var BuildDate string 11 | 12 | // prepareVersionInfo sets some runtime version when the version value 13 | // was not injected by the build into the binary (e.g. go get). 14 | // This returns currently "(devel)" but not an effective version until 15 | // https://github.com/golang/go/issues/29814 gets resolved. 16 | func prepareVersionInfo() { 17 | if Version == "" { 18 | // bi, _ := debug.ReadBuildInfo() 19 | // Version = bi.Main.Version 20 | // TODO use the debug information when it will provide more details 21 | // It seems to panic with Go 1.13. 22 | Version = "dev" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /cmd/gosecutil/tools.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package main 16 | 17 | import ( 18 | "flag" 19 | "fmt" 20 | "go/ast" 21 | "go/importer" 22 | "go/parser" 23 | "go/token" 24 | "go/types" 25 | "os" 26 | "sort" 27 | "strings" 28 | ) 29 | 30 | type command func(args ...string) 31 | type utilities struct { 32 | commands map[string]command 33 | call []string 34 | } 35 | 36 | // Custom commands / utilities to run instead of default analyzer 37 | func newUtils() *utilities { 38 | utils := make(map[string]command) 39 | utils["ast"] = dumpAst 40 | utils["callobj"] = dumpCallObj 41 | utils["uses"] = dumpUses 42 | utils["types"] = dumpTypes 43 | utils["defs"] = dumpDefs 44 | utils["comments"] = dumpComments 45 | utils["imports"] = dumpImports 46 | return &utilities{utils, make([]string, 0)} 47 | } 48 | 49 | func (u *utilities) String() string { 50 | keys := make([]string, 0, len(u.commands)) 51 | for k := range u.commands { 52 | keys = append(keys, k) 53 | } 54 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 55 | return strings.Join(keys, ", ") 56 | } 57 | 58 | func (u *utilities) Set(opt string) error { 59 | if _, ok := u.commands[opt]; !ok { 60 | return fmt.Errorf("valid tools are: %s", u.String()) 61 | 62 | } 63 | u.call = append(u.call, opt) 64 | return nil 65 | } 66 | 67 | func (u *utilities) run(args ...string) { 68 | for _, util := range u.call { 69 | if cmd, ok := u.commands[util]; ok { 70 | cmd(args...) 71 | } 72 | } 73 | } 74 | 75 | func shouldSkip(path string) bool { 76 | st, e := os.Stat(path) 77 | if e != nil { 78 | // #nosec 79 | fmt.Fprintf(os.Stderr, "Skipping: %s - %s\n", path, e) 80 | return true 81 | } 82 | if st.IsDir() { 83 | // #nosec 84 | fmt.Fprintf(os.Stderr, "Skipping: %s - directory\n", path) 85 | return true 86 | } 87 | return false 88 | } 89 | 90 | func dumpAst(files ...string) { 91 | for _, arg := range files { 92 | // Ensure file exists and not a directory 93 | if shouldSkip(arg) { 94 | continue 95 | } 96 | 97 | // Create the AST by parsing src. 98 | fset := token.NewFileSet() // positions are relative to fset 99 | f, err := parser.ParseFile(fset, arg, nil, 0) 100 | if err != nil { 101 | // #nosec 102 | fmt.Fprintf(os.Stderr, "Unable to parse file %s\n", err) 103 | continue 104 | } 105 | 106 | // Print the AST. #nosec 107 | ast.Print(fset, f) 108 | } 109 | } 110 | 111 | type context struct { 112 | fileset *token.FileSet 113 | comments ast.CommentMap 114 | info *types.Info 115 | pkg *types.Package 116 | config *types.Config 117 | root *ast.File 118 | } 119 | 120 | func createContext(filename string) *context { 121 | fileset := token.NewFileSet() 122 | root, e := parser.ParseFile(fileset, filename, nil, parser.ParseComments) 123 | if e != nil { 124 | // #nosec 125 | fmt.Fprintf(os.Stderr, "Unable to parse file: %s. Reason: %s\n", filename, e) 126 | return nil 127 | } 128 | comments := ast.NewCommentMap(fileset, root, root.Comments) 129 | info := &types.Info{ 130 | Types: make(map[ast.Expr]types.TypeAndValue), 131 | Defs: make(map[*ast.Ident]types.Object), 132 | Uses: make(map[*ast.Ident]types.Object), 133 | Selections: make(map[*ast.SelectorExpr]*types.Selection), 134 | Scopes: make(map[ast.Node]*types.Scope), 135 | Implicits: make(map[ast.Node]types.Object), 136 | } 137 | config := types.Config{Importer: importer.Default()} 138 | pkg, e := config.Check("main.go", fileset, []*ast.File{root}, info) 139 | if e != nil { 140 | // #nosec 141 | fmt.Fprintf(os.Stderr, "Type check failed for file: %s. Reason: %s\n", filename, e) 142 | return nil 143 | } 144 | return &context{fileset, comments, info, pkg, &config, root} 145 | } 146 | 147 | func printObject(obj types.Object) { 148 | fmt.Println("OBJECT") 149 | if obj == nil { 150 | fmt.Println("object is nil") 151 | return 152 | } 153 | fmt.Printf(" Package = %v\n", obj.Pkg()) 154 | if obj.Pkg() != nil { 155 | fmt.Println(" Path = ", obj.Pkg().Path()) 156 | fmt.Println(" Name = ", obj.Pkg().Name()) 157 | fmt.Println(" String = ", obj.Pkg().String()) 158 | } 159 | fmt.Printf(" Name = %v\n", obj.Name()) 160 | fmt.Printf(" Type = %v\n", obj.Type()) 161 | fmt.Printf(" Id = %v\n", obj.Id()) 162 | } 163 | 164 | func checkContext(ctx *context, file string) bool { 165 | // #nosec 166 | if ctx == nil { 167 | fmt.Fprintln(os.Stderr, "Failed to create context for file: ", file) 168 | return false 169 | } 170 | return true 171 | } 172 | 173 | func dumpCallObj(files ...string) { 174 | 175 | for _, file := range files { 176 | if shouldSkip(file) { 177 | continue 178 | } 179 | context := createContext(file) 180 | if !checkContext(context, file) { 181 | return 182 | } 183 | ast.Inspect(context.root, func(n ast.Node) bool { 184 | var obj types.Object 185 | switch node := n.(type) { 186 | case *ast.Ident: 187 | obj = context.info.ObjectOf(node) //context.info.Uses[node] 188 | case *ast.SelectorExpr: 189 | obj = context.info.ObjectOf(node.Sel) //context.info.Uses[node.Sel] 190 | default: 191 | obj = nil 192 | } 193 | if obj != nil { 194 | printObject(obj) 195 | } 196 | return true 197 | }) 198 | } 199 | } 200 | 201 | func dumpUses(files ...string) { 202 | for _, file := range files { 203 | if shouldSkip(file) { 204 | continue 205 | } 206 | context := createContext(file) 207 | if !checkContext(context, file) { 208 | return 209 | } 210 | idents := make([]*ast.Ident, 0, len(context.info.Uses)) 211 | for ident := range context.info.Uses { 212 | idents = append(idents, ident) 213 | } 214 | sort.Slice(idents, func(i, j int) bool { return idents[i].String() < idents[j].String() }) 215 | for _, ident := range idents { 216 | fmt.Printf("IDENT: %v, OBJECT: %v\n", ident, context.info.Uses[ident]) 217 | } 218 | } 219 | } 220 | 221 | func dumpTypes(files ...string) { 222 | for _, file := range files { 223 | if shouldSkip(file) { 224 | continue 225 | } 226 | context := createContext(file) 227 | if !checkContext(context, file) { 228 | return 229 | } 230 | exprs := make([]ast.Expr, 0, len(context.info.Types)) 231 | for expr := range context.info.Types { 232 | exprs = append(exprs, expr) 233 | } 234 | sort.Slice(exprs, func(i, j int) bool { return fmt.Sprintf("%v", exprs[i]) < fmt.Sprintf("%v", exprs[j]) }) 235 | for _, expr := range exprs { 236 | fmt.Printf("EXPR: %v, TYPE: %v\n", expr, context.info.Types[expr]) 237 | } 238 | } 239 | } 240 | 241 | func dumpDefs(files ...string) { 242 | for _, file := range files { 243 | if shouldSkip(file) { 244 | continue 245 | } 246 | context := createContext(file) 247 | if !checkContext(context, file) { 248 | return 249 | } 250 | idents := make([]*ast.Ident, 0, len(context.info.Defs)) 251 | for ident := range context.info.Defs { 252 | idents = append(idents, ident) 253 | } 254 | sort.Slice(idents, func(i, j int) bool { return idents[i].String() < idents[j].String() }) 255 | for _, ident := range idents { 256 | fmt.Printf("IDENT: %v, OBJECT: %v\n", ident, context.info.Defs[ident]) 257 | } 258 | } 259 | } 260 | 261 | func dumpComments(files ...string) { 262 | for _, file := range files { 263 | if shouldSkip(file) { 264 | continue 265 | } 266 | context := createContext(file) 267 | if !checkContext(context, file) { 268 | return 269 | } 270 | for _, group := range context.comments.Comments() { 271 | fmt.Println(group.Text()) 272 | } 273 | } 274 | } 275 | 276 | func dumpImports(files ...string) { 277 | for _, file := range files { 278 | if shouldSkip(file) { 279 | continue 280 | } 281 | context := createContext(file) 282 | if !checkContext(context, file) { 283 | return 284 | } 285 | for _, pkg := range context.pkg.Imports() { 286 | fmt.Println(pkg.Path(), pkg.Name()) 287 | for _, name := range pkg.Scope().Names() { 288 | fmt.Println(" => ", name) 289 | } 290 | } 291 | } 292 | } 293 | 294 | func main() { 295 | tools := newUtils() 296 | flag.Var(tools, "tool", "Utils to assist with rule development") 297 | flag.Parse() 298 | 299 | if len(tools.call) > 0 { 300 | tools.run(flag.Args()...) 301 | os.Exit(0) 302 | } 303 | } 304 | -------------------------------------------------------------------------------- /cmd/tlsconfig/header_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "text/template" 4 | 5 | var generatedHeaderTmpl = template.Must(template.New("generated").Parse(` 6 | package {{.}} 7 | 8 | import ( 9 | "go/ast" 10 | 11 | "github.com/cosmos/gosec/v2" 12 | ) 13 | `)) 14 | -------------------------------------------------------------------------------- /cmd/tlsconfig/rule_template.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "text/template" 4 | 5 | var generatedRuleTmpl = template.Must(template.New("generated").Parse(` 6 | // New{{.Name}}TLSCheck creates a check for {{.Name}} TLS ciphers 7 | // DO NOT EDIT - generated by tlsconfig tool 8 | func New{{.Name}}TLSCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 9 | return &insecureConfigTLS{ 10 | MetaData: gosec.MetaData{ID: id}, 11 | requiredType: "crypto/tls.Config", 12 | MinVersion: {{ .MinVersion }}, 13 | MaxVersion: {{ .MaxVersion }}, 14 | goodCiphers: []string{ 15 | {{range $cipherName := .Ciphers }} "{{$cipherName}}", 16 | {{end}} 17 | }, 18 | }, []ast.Node{(*ast.CompositeLit)(nil)} 19 | } 20 | `)) 21 | -------------------------------------------------------------------------------- /cmd/tlsconfig/tls_version_go12_go13.go: -------------------------------------------------------------------------------- 1 | //go:build go1.12 && !go1.14 2 | // +build go1.12,!go1.14 3 | 4 | // This file can be removed once go1.13 is no longer supported 5 | 6 | package main 7 | 8 | import ( 9 | "crypto/tls" 10 | "sort" 11 | ) 12 | 13 | func mapTLSVersions(tlsVersions []string) []int { 14 | var versions []int 15 | for _, tlsVersion := range tlsVersions { 16 | switch tlsVersion { 17 | case "TLSv1.3": 18 | versions = append(versions, tls.VersionTLS13) 19 | case "TLSv1.2": 20 | versions = append(versions, tls.VersionTLS12) 21 | case "TLSv1.1": 22 | versions = append(versions, tls.VersionTLS11) 23 | case "TLSv1": 24 | versions = append(versions, tls.VersionTLS10) 25 | case "SSLv3": 26 | // unsupported from go1.14 27 | versions = append(versions, tls.VersionSSL30) 28 | default: 29 | continue 30 | } 31 | } 32 | sort.Ints(versions) 33 | return versions 34 | } 35 | -------------------------------------------------------------------------------- /cmd/tlsconfig/tls_version_go14.go: -------------------------------------------------------------------------------- 1 | //go:build go1.14 || !go1.11 2 | // +build go1.14 !go1.11 3 | 4 | // main 5 | package main 6 | 7 | import ( 8 | "crypto/tls" 9 | "sort" 10 | ) 11 | 12 | func mapTLSVersions(tlsVersions []string) []int { 13 | var versions []int 14 | for _, tlsVersion := range tlsVersions { 15 | switch tlsVersion { 16 | case "TLSv1.3": 17 | versions = append(versions, tls.VersionTLS13) 18 | case "TLSv1.2": 19 | versions = append(versions, tls.VersionTLS12) 20 | case "TLSv1.1": 21 | versions = append(versions, tls.VersionTLS11) 22 | case "TLSv1": 23 | versions = append(versions, tls.VersionTLS10) 24 | default: 25 | continue 26 | } 27 | } 28 | sort.Ints(versions) 29 | return versions 30 | } 31 | -------------------------------------------------------------------------------- /cmd/tlsconfig/tlsconfig.go: -------------------------------------------------------------------------------- 1 | //go:build go1.12 2 | // +build go1.12 3 | 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "encoding/json" 9 | "errors" 10 | "flag" 11 | "fmt" 12 | "go/format" 13 | "io/ioutil" 14 | "log" 15 | "net/http" 16 | "path/filepath" 17 | "strings" 18 | 19 | "github.com/mozilla/tls-observatory/constants" 20 | ) 21 | 22 | var ( 23 | pkg = flag.String("pkg", "rules", "package name to be added to the output file") 24 | outputFile = flag.String("outputFile", "tls_config.go", "name of the output file") 25 | ) 26 | 27 | // TLSConfURL url where Mozilla publishes the TLS ciphers recommendations 28 | const TLSConfURL = "https://statics.tls.security.mozilla.org/server-side-tls-conf.json" 29 | 30 | // ServerSideTLSJson contains all the available configurations and the version of the current document. 31 | type ServerSideTLSJson struct { 32 | Configurations map[string]Configuration `json:"configurations"` 33 | Version float64 `json:"version"` 34 | } 35 | 36 | // Configuration represents configurations levels declared by the Mozilla server-side-tls 37 | // see https://wiki.mozilla.org/Security/Server_Side_TLS 38 | type Configuration struct { 39 | OpenSSLCiphersuites []string `json:"openssl_ciphersuites"` 40 | OpenSSLCiphers []string `json:"openssl_ciphers"` 41 | TLSVersions []string `json:"tls_versions"` 42 | TLSCurves []string `json:"tls_curves"` 43 | CertificateTypes []string `json:"certificate_types"` 44 | CertificateCurves []string `json:"certificate_curves"` 45 | CertificateSignatures []string `json:"certificate_signatures"` 46 | RsaKeySize float64 `json:"rsa_key_size"` 47 | DHParamSize float64 `json:"dh_param_size"` 48 | ECDHParamSize float64 `json:"ecdh_param_size"` 49 | HstsMinAge float64 `json:"hsts_min_age"` 50 | OldestClients []string `json:"oldest_clients"` 51 | OCSPStample bool `json:"ocsp_staple"` 52 | ServerPreferedOrder bool `json:"server_preferred_order"` 53 | MaxCertLifespan float64 `json:"maximum_certificate_lifespan"` 54 | } 55 | 56 | type goCipherConfiguration struct { 57 | Name string 58 | Ciphers []string 59 | MinVersion string 60 | MaxVersion string 61 | } 62 | 63 | type goTLSConfiguration struct { 64 | cipherConfigs []goCipherConfiguration 65 | } 66 | 67 | // getTLSConfFromURL retrieves the json containing the TLS configurations from the specified URL. 68 | func getTLSConfFromURL(url string) (*ServerSideTLSJson, error) { 69 | r, err := http.Get(url) // #nosec G107 70 | if err != nil { 71 | return nil, err 72 | } 73 | defer r.Body.Close() 74 | 75 | var sstls ServerSideTLSJson 76 | err = json.NewDecoder(r.Body).Decode(&sstls) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return &sstls, nil 82 | } 83 | 84 | func getGoCipherConfig(name string, sstls ServerSideTLSJson) (goCipherConfiguration, error) { 85 | cipherConf := goCipherConfiguration{Name: strings.Title(name)} 86 | conf, ok := sstls.Configurations[name] 87 | if !ok { 88 | return cipherConf, fmt.Errorf("TLS configuration '%s' not found", name) 89 | } 90 | 91 | // These ciphers are already defined in IANA format 92 | cipherConf.Ciphers = append(cipherConf.Ciphers, conf.OpenSSLCiphersuites...) 93 | 94 | for _, cipherName := range conf.OpenSSLCiphers { 95 | cipherSuite, ok := constants.CipherSuites[cipherName] 96 | if !ok { 97 | log.Printf("'%s' cipher is not available in crypto/tls package\n", cipherName) 98 | } 99 | if len(cipherSuite.IANAName) > 0 { 100 | cipherConf.Ciphers = append(cipherConf.Ciphers, cipherSuite.IANAName) 101 | if len(cipherSuite.NSSName) > 0 && cipherSuite.NSSName != cipherSuite.IANAName { 102 | cipherConf.Ciphers = append(cipherConf.Ciphers, cipherSuite.NSSName) 103 | } 104 | } 105 | } 106 | 107 | versions := mapTLSVersions(conf.TLSVersions) 108 | if len(versions) > 0 { 109 | cipherConf.MinVersion = fmt.Sprintf("0x%04x", versions[0]) 110 | cipherConf.MaxVersion = fmt.Sprintf("0x%04x", versions[len(versions)-1]) 111 | } else { 112 | return cipherConf, fmt.Errorf("No TLS versions found for configuration '%s'", name) 113 | } 114 | return cipherConf, nil 115 | } 116 | 117 | func getGoTLSConf() (goTLSConfiguration, error) { 118 | sstls, err := getTLSConfFromURL(TLSConfURL) 119 | if err != nil || sstls == nil { 120 | msg := fmt.Sprintf("Could not load the Server Side TLS configuration from Mozilla's website. Check the URL: %s. Error: %v\n", 121 | TLSConfURL, err) 122 | panic(msg) 123 | } 124 | 125 | tlsConfg := goTLSConfiguration{} 126 | 127 | modern, err := getGoCipherConfig("modern", *sstls) 128 | if err != nil { 129 | return tlsConfg, err 130 | } 131 | tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, modern) 132 | 133 | intermediate, err := getGoCipherConfig("intermediate", *sstls) 134 | if err != nil { 135 | return tlsConfg, err 136 | } 137 | tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, intermediate) 138 | 139 | old, err := getGoCipherConfig("old", *sstls) 140 | if err != nil { 141 | return tlsConfg, err 142 | } 143 | tlsConfg.cipherConfigs = append(tlsConfg.cipherConfigs, old) 144 | 145 | return tlsConfg, nil 146 | } 147 | 148 | func getCurrentDir() (string, error) { 149 | dir := "." 150 | if args := flag.Args(); len(args) == 1 { 151 | dir = args[0] 152 | } else if len(args) > 1 { 153 | return "", errors.New("only one directory at a time") 154 | } 155 | dir, err := filepath.Abs(dir) 156 | if err != nil { 157 | return "", err 158 | } 159 | return dir, nil 160 | } 161 | 162 | func main() { 163 | dir, err := getCurrentDir() 164 | if err != nil { 165 | log.Fatalln(err) 166 | } 167 | tlsConfig, err := getGoTLSConf() 168 | if err != nil { 169 | log.Fatalln(err) 170 | } 171 | 172 | var buf bytes.Buffer 173 | err = generatedHeaderTmpl.Execute(&buf, *pkg) 174 | if err != nil { 175 | log.Fatalf("Failed to generate the header: %v", err) 176 | } 177 | for _, cipherConfig := range tlsConfig.cipherConfigs { 178 | err := generatedRuleTmpl.Execute(&buf, cipherConfig) 179 | if err != nil { 180 | log.Fatalf("Failed to generated the cipher config: %v", err) 181 | } 182 | } 183 | 184 | src, err := format.Source(buf.Bytes()) 185 | if err != nil { 186 | log.Printf("warnings: Failed to format the code: %v", err) 187 | src = buf.Bytes() 188 | } 189 | 190 | outputPath := filepath.Join(dir, *outputFile) 191 | if err := ioutil.WriteFile(outputPath, src, 0644); err != nil { 192 | log.Fatalf("Writing output: %s", err) 193 | } // #nosec G306 194 | } 195 | -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package gosec 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "sort" 10 | ) 11 | 12 | const ( 13 | // Globals are applicable to all rules and used for general 14 | // configuration settings for gosec. 15 | Globals = "global" 16 | ) 17 | 18 | // GlobalOption defines the name of the global options 19 | type GlobalOption string 20 | 21 | const ( 22 | // Nosec global option for #nosec directive 23 | Nosec GlobalOption = "nosec" 24 | // Audit global option which indicates that gosec runs in audit mode 25 | Audit GlobalOption = "audit" 26 | // NoSecAlternative global option alternative for #nosec directive 27 | NoSecAlternative GlobalOption = "#nosec" 28 | ) 29 | 30 | // Config is used to provide configuration and customization to each of the rules. 31 | type Config map[string]interface{} 32 | 33 | // NewConfig initializes a new configuration instance. The configuration data then 34 | // needs to be loaded via c.ReadFrom(strings.NewReader("config data")) 35 | // or from a *os.File. 36 | func NewConfig() Config { 37 | cfg := make(Config) 38 | cfg[Globals] = make(map[GlobalOption]string) 39 | return cfg 40 | } 41 | 42 | func (c Config) keyToGlobalOptions(key string) GlobalOption { 43 | return GlobalOption(key) 44 | } 45 | 46 | func (c Config) convertGlobals() { 47 | if globals, ok := c[Globals]; ok { 48 | if settings, ok := globals.(map[string]interface{}); ok { 49 | validGlobals := map[GlobalOption]string{} 50 | keys := make([]string, 0, len(settings)) 51 | for k := range settings { 52 | keys = append(keys, k) 53 | } 54 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 55 | for _, k := range keys { 56 | validGlobals[c.keyToGlobalOptions(k)] = fmt.Sprintf("%v", settings[k]) 57 | } 58 | c[Globals] = validGlobals 59 | } 60 | } 61 | } 62 | 63 | // ReadFrom implements the io.ReaderFrom interface. This 64 | // should be used with io.Reader to load configuration from 65 | // file or from string etc. 66 | func (c Config) ReadFrom(r io.Reader) (int64, error) { 67 | data, err := ioutil.ReadAll(r) 68 | if err != nil { 69 | return int64(len(data)), err 70 | } 71 | if err = json.Unmarshal(data, &c); err != nil { 72 | return int64(len(data)), err 73 | } 74 | c.convertGlobals() 75 | return int64(len(data)), nil 76 | } 77 | 78 | // WriteTo implements the io.WriteTo interface. This should 79 | // be used to save or print out the configuration information. 80 | func (c Config) WriteTo(w io.Writer) (int64, error) { 81 | data, err := json.Marshal(c) 82 | if err != nil { 83 | return int64(len(data)), err 84 | } 85 | return io.Copy(w, bytes.NewReader(data)) 86 | } 87 | 88 | // Get returns the configuration section for the supplied key 89 | func (c Config) Get(section string) (interface{}, error) { 90 | settings, found := c[section] 91 | if !found { 92 | return nil, fmt.Errorf("Section %s not in configuration", section) 93 | } 94 | return settings, nil 95 | } 96 | 97 | // Set section in the configuration to specified value 98 | func (c Config) Set(section string, value interface{}) { 99 | c[section] = value 100 | } 101 | 102 | // GetGlobal returns value associated with global configuration option 103 | func (c Config) GetGlobal(option GlobalOption) (string, error) { 104 | if globals, ok := c[Globals]; ok { 105 | if settings, ok := globals.(map[GlobalOption]string); ok { 106 | if value, ok := settings[option]; ok { 107 | return value, nil 108 | } 109 | return "", fmt.Errorf("global setting for %s not found", option) 110 | } 111 | } 112 | return "", fmt.Errorf("no global config options found") 113 | } 114 | 115 | // SetGlobal associates a value with a global configuration option 116 | func (c Config) SetGlobal(option GlobalOption, value string) { 117 | if globals, ok := c[Globals]; ok { 118 | if settings, ok := globals.(map[GlobalOption]string); ok { 119 | settings[option] = value 120 | } 121 | } 122 | } 123 | 124 | // IsGlobalEnabled checks if a global option is enabled 125 | func (c Config) IsGlobalEnabled(option GlobalOption) (bool, error) { 126 | value, err := c.GetGlobal(option) 127 | if err != nil { 128 | return false, err 129 | } 130 | return (value == "true" || value == "enabled"), nil 131 | } 132 | -------------------------------------------------------------------------------- /config_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "bytes" 5 | "strings" 6 | 7 | "github.com/cosmos/gosec/v2" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | var _ = Describe("Configuration", func() { 13 | var configuration gosec.Config 14 | BeforeEach(func() { 15 | configuration = gosec.NewConfig() 16 | }) 17 | 18 | Context("when loading from disk", func() { 19 | 20 | It("should be possible to load configuration from a file", func() { 21 | json := `{"G101": {}}` 22 | buffer := bytes.NewBufferString(json) 23 | nread, err := configuration.ReadFrom(buffer) 24 | Expect(nread).Should(Equal(int64(len(json)))) 25 | Expect(err).ShouldNot(HaveOccurred()) 26 | }) 27 | 28 | It("should return an error if configuration file is invalid", func() { 29 | var err error 30 | invalidBuffer := bytes.NewBuffer([]byte{0xc0, 0xff, 0xee}) 31 | _, err = configuration.ReadFrom(invalidBuffer) 32 | Expect(err).Should(HaveOccurred()) 33 | 34 | emptyBuffer := bytes.NewBuffer([]byte{}) 35 | _, err = configuration.ReadFrom(emptyBuffer) 36 | Expect(err).Should(HaveOccurred()) 37 | }) 38 | 39 | }) 40 | 41 | Context("when saving to disk", func() { 42 | It("should be possible to save an empty configuration to file", func() { 43 | expected := `{"global":{}}` 44 | buffer := bytes.NewBuffer([]byte{}) 45 | nbytes, err := configuration.WriteTo(buffer) 46 | Expect(int(nbytes)).Should(Equal(len(expected))) 47 | Expect(err).ShouldNot(HaveOccurred()) 48 | Expect(buffer.String()).Should(Equal(expected)) 49 | }) 50 | 51 | It("should be possible to save configuration to file", func() { 52 | 53 | configuration.Set("G101", map[string]string{ 54 | "mode": "strict", 55 | }) 56 | 57 | buffer := bytes.NewBuffer([]byte{}) 58 | nbytes, err := configuration.WriteTo(buffer) 59 | Expect(int(nbytes)).ShouldNot(BeZero()) 60 | Expect(err).ShouldNot(HaveOccurred()) 61 | Expect(buffer.String()).Should(Equal(`{"G101":{"mode":"strict"},"global":{}}`)) 62 | 63 | }) 64 | }) 65 | 66 | Context("when configuring rules", func() { 67 | 68 | It("should be possible to get configuration for a rule", func() { 69 | settings := map[string]string{ 70 | "ciphers": "AES256-GCM", 71 | } 72 | configuration.Set("G101", settings) 73 | 74 | retrieved, err := configuration.Get("G101") 75 | Expect(err).ShouldNot(HaveOccurred()) 76 | Expect(retrieved).Should(HaveKeyWithValue("ciphers", "AES256-GCM")) 77 | Expect(retrieved).ShouldNot(HaveKey("foobar")) 78 | }) 79 | }) 80 | 81 | Context("when using global configuration options", func() { 82 | It("should have a default global section", func() { 83 | settings, err := configuration.Get("global") 84 | Expect(err).Should(BeNil()) 85 | expectedType := make(map[gosec.GlobalOption]string) 86 | Expect(settings).Should(BeAssignableToTypeOf(expectedType)) 87 | }) 88 | 89 | It("should save global settings to correct section", func() { 90 | configuration.SetGlobal(gosec.Nosec, "enabled") 91 | settings, err := configuration.Get("global") 92 | Expect(err).Should(BeNil()) 93 | if globals, ok := settings.(map[gosec.GlobalOption]string); ok { 94 | Expect(globals["nosec"]).Should(MatchRegexp("enabled")) 95 | } else { 96 | Fail("globals are not defined as map") 97 | } 98 | 99 | setValue, err := configuration.GetGlobal(gosec.Nosec) 100 | Expect(err).Should(BeNil()) 101 | Expect(setValue).Should(MatchRegexp("enabled")) 102 | }) 103 | 104 | It("should find global settings which are enabled", func() { 105 | configuration.SetGlobal(gosec.Nosec, "enabled") 106 | enabled, err := configuration.IsGlobalEnabled(gosec.Nosec) 107 | Expect(err).Should(BeNil()) 108 | Expect(enabled).Should(BeTrue()) 109 | }) 110 | 111 | It("should parse the global settings of type string from file", func() { 112 | config := ` 113 | { 114 | "global": { 115 | "nosec": "enabled" 116 | } 117 | }` 118 | cfg := gosec.NewConfig() 119 | _, err := cfg.ReadFrom(strings.NewReader(config)) 120 | Expect(err).Should(BeNil()) 121 | 122 | value, err := cfg.GetGlobal(gosec.Nosec) 123 | Expect(err).Should(BeNil()) 124 | Expect(value).Should(Equal("enabled")) 125 | }) 126 | It("should parse the global settings of other types from file", func() { 127 | config := ` 128 | { 129 | "global": { 130 | "nosec": true 131 | } 132 | }` 133 | cfg := gosec.NewConfig() 134 | _, err := cfg.ReadFrom(strings.NewReader(config)) 135 | Expect(err).Should(BeNil()) 136 | 137 | value, err := cfg.GetGlobal(gosec.Nosec) 138 | Expect(err).Should(BeNil()) 139 | Expect(value).Should(Equal("true")) 140 | }) 141 | }) 142 | }) 143 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Expand the arguments into an array of strings. This is requires because the GitHub action 4 | # provides all arguments concatenated as a single string. 5 | ARGS=("$@") 6 | 7 | /bin/gosec ${ARGS[*]} 8 | -------------------------------------------------------------------------------- /errors.go: -------------------------------------------------------------------------------- 1 | package gosec 2 | 3 | import ( 4 | "sort" 5 | ) 6 | 7 | // Error is used when there are golang errors while parsing the AST 8 | type Error struct { 9 | Line int `json:"line"` 10 | Column int `json:"column"` 11 | Err string `json:"error"` 12 | } 13 | 14 | // NewError creates Error object 15 | func NewError(line, column int, err string) *Error { 16 | return &Error{ 17 | Line: line, 18 | Column: column, 19 | Err: err, 20 | } 21 | } 22 | 23 | // sortErrors sorts the golang errors by line 24 | func sortErrors(allErrors map[string][]Error) { 25 | keys := make([]string, 0, len(allErrors)) 26 | for key := range allErrors { 27 | keys = append(keys, key) 28 | } 29 | sort.Slice(keys, func(i, j int) bool { return keys[i] < keys[j] }) 30 | for _, key := range keys { 31 | errors := allErrors[key] 32 | sort.Slice(errors, func(i, j int) bool { 33 | if errors[i].Line == errors[j].Line { 34 | return errors[i].Column <= errors[j].Column 35 | } 36 | return errors[i].Line < errors[j].Line 37 | }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | go 1.17 2 | 3 | module github.com/cosmos/gosec/v2 4 | 5 | require ( 6 | github.com/google/go-cmp v0.5.8 7 | github.com/gookit/color v1.5.2 8 | github.com/mozilla/tls-observatory v0.0.0-20210609171429-7bc42856d2e5 9 | github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354 10 | github.com/onsi/ginkgo/v2 v2.2.0 11 | github.com/onsi/gomega v1.20.1 12 | golang.org/x/tools v0.1.12 13 | gopkg.in/yaml.v2 v2.4.0 14 | ) 15 | 16 | require ( 17 | github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect 18 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect 19 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b // indirect 20 | golang.org/x/sys v0.0.0-20220913175220-63ea55921009 // indirect 21 | golang.org/x/text v0.3.7 // indirect 22 | gopkg.in/yaml.v3 v3.0.1 // indirect 23 | ) 24 | -------------------------------------------------------------------------------- /gosec_suite_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestGosec(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "gosec Suite") 13 | } 14 | -------------------------------------------------------------------------------- /helpers_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "go/ast" 5 | "io/ioutil" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | 10 | "github.com/cosmos/gosec/v2" 11 | "github.com/cosmos/gosec/v2/testutils" 12 | . "github.com/onsi/ginkgo/v2" 13 | . "github.com/onsi/gomega" 14 | ) 15 | 16 | var _ = Describe("Helpers", func() { 17 | Context("when listing pacakge paths", func() { 18 | var dir string 19 | JustBeforeEach(func() { 20 | var err error 21 | dir, err = ioutil.TempDir("", "gosec") 22 | Expect(err).ShouldNot(HaveOccurred()) 23 | _, err = ioutil.TempFile(dir, "test*.go") 24 | Expect(err).ShouldNot(HaveOccurred()) 25 | }) 26 | AfterEach(func() { 27 | err := os.RemoveAll(dir) 28 | Expect(err).ShouldNot(HaveOccurred()) 29 | }) 30 | It("should return the root directory as package path", func() { 31 | paths, err := gosec.PackagePaths(dir, nil) 32 | Expect(err).ShouldNot(HaveOccurred()) 33 | Expect(paths).Should(Equal([]string{dir})) 34 | }) 35 | It("should return the package package path", func() { 36 | paths, err := gosec.PackagePaths(dir+"/...", nil) 37 | Expect(err).ShouldNot(HaveOccurred()) 38 | Expect(paths).Should(Equal([]string{dir})) 39 | }) 40 | It("should exclude folder", func() { 41 | nested := dir + "/vendor" 42 | err := os.Mkdir(nested, 0755) 43 | Expect(err).ShouldNot(HaveOccurred()) 44 | _, err = os.Create(nested + "/test.go") 45 | Expect(err).ShouldNot(HaveOccurred()) 46 | exclude, err := regexp.Compile(`([\\/])?vendor([\\/])?`) 47 | Expect(err).ShouldNot(HaveOccurred()) 48 | paths, err := gosec.PackagePaths(dir+"/...", []*regexp.Regexp{exclude}) 49 | Expect(err).ShouldNot(HaveOccurred()) 50 | Expect(paths).Should(Equal([]string{dir})) 51 | }) 52 | It("should be empty when folder does not exist", func() { 53 | nested := dir + "/test" 54 | paths, err := gosec.PackagePaths(nested+"/...", nil) 55 | Expect(err).ShouldNot(HaveOccurred()) 56 | Expect(paths).Should(BeEmpty()) 57 | }) 58 | }) 59 | 60 | Context("when getting the root path", func() { 61 | It("should return the absolute path from relative path", func() { 62 | base := "test" 63 | cwd, err := os.Getwd() 64 | Expect(err).ShouldNot(HaveOccurred()) 65 | root, err := gosec.RootPath(base) 66 | Expect(err).ShouldNot(HaveOccurred()) 67 | Expect(root).Should(Equal(filepath.Join(cwd, base))) 68 | }) 69 | It("should retrun the absolute path from ellipsis path", func() { 70 | base := "test" 71 | cwd, err := os.Getwd() 72 | Expect(err).ShouldNot(HaveOccurred()) 73 | root, err := gosec.RootPath(filepath.Join(base, "...")) 74 | Expect(err).ShouldNot(HaveOccurred()) 75 | Expect(root).Should(Equal(filepath.Join(cwd, base))) 76 | }) 77 | }) 78 | 79 | Context("when excluding the dirs", func() { 80 | It("should create a proper regexp", func() { 81 | r := gosec.ExcludedDirsRegExp([]string{"test"}) 82 | Expect(len(r)).Should(Equal(1)) 83 | match := r[0].MatchString("/home/go/src/project/test/pkg") 84 | Expect(match).Should(BeTrue()) 85 | match = r[0].MatchString("/home/go/src/project/vendor/pkg") 86 | Expect(match).Should(BeFalse()) 87 | }) 88 | 89 | It("should create no regexp when dir list is empty", func() { 90 | r := gosec.ExcludedDirsRegExp(nil) 91 | Expect(len(r)).Should(Equal(0)) 92 | r = gosec.ExcludedDirsRegExp([]string{}) 93 | Expect(len(r)).Should(Equal(0)) 94 | }) 95 | }) 96 | 97 | Context("when getting call info", func() { 98 | It("should return the type and call name for selector expression", func() { 99 | pkg := testutils.NewTestPackage() 100 | defer pkg.Close() 101 | pkg.AddFile("main.go", ` 102 | package main 103 | 104 | import( 105 | "bytes" 106 | ) 107 | 108 | func main() { 109 | b := new(bytes.Buffer) 110 | _, err := b.WriteString("test") 111 | if err != nil { 112 | panic(err) 113 | } 114 | } 115 | `) 116 | ctx := pkg.CreateContext("main.go") 117 | result := map[string]string{} 118 | visitor := testutils.NewMockVisitor() 119 | visitor.Context = ctx 120 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 121 | typeName, call, err := gosec.GetCallInfo(n, ctx) 122 | if err == nil { 123 | result[typeName] = call 124 | } 125 | return true 126 | } 127 | ast.Walk(visitor, ctx.Root) 128 | 129 | Expect(result).Should(HaveKeyWithValue("*bytes.Buffer", "WriteString")) 130 | }) 131 | 132 | It("should return the type and call name for new selector expression", func() { 133 | pkg := testutils.NewTestPackage() 134 | defer pkg.Close() 135 | pkg.AddFile("main.go", ` 136 | package main 137 | 138 | import( 139 | "bytes" 140 | ) 141 | 142 | func main() { 143 | _, err := new(bytes.Buffer).WriteString("test") 144 | if err != nil { 145 | panic(err) 146 | } 147 | } 148 | `) 149 | ctx := pkg.CreateContext("main.go") 150 | result := map[string]string{} 151 | visitor := testutils.NewMockVisitor() 152 | visitor.Context = ctx 153 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 154 | typeName, call, err := gosec.GetCallInfo(n, ctx) 155 | if err == nil { 156 | result[typeName] = call 157 | } 158 | return true 159 | } 160 | ast.Walk(visitor, ctx.Root) 161 | 162 | Expect(result).Should(HaveKeyWithValue("bytes.Buffer", "WriteString")) 163 | }) 164 | 165 | It("should return the type and call name for function selector expression", func() { 166 | pkg := testutils.NewTestPackage() 167 | defer pkg.Close() 168 | pkg.AddFile("main.go", ` 169 | package main 170 | 171 | import( 172 | "bytes" 173 | ) 174 | 175 | func createBuffer() *bytes.Buffer { 176 | return new(bytes.Buffer) 177 | } 178 | 179 | func main() { 180 | _, err := createBuffer().WriteString("test") 181 | if err != nil { 182 | panic(err) 183 | } 184 | } 185 | `) 186 | ctx := pkg.CreateContext("main.go") 187 | result := map[string]string{} 188 | visitor := testutils.NewMockVisitor() 189 | visitor.Context = ctx 190 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 191 | typeName, call, err := gosec.GetCallInfo(n, ctx) 192 | if err == nil { 193 | result[typeName] = call 194 | } 195 | return true 196 | } 197 | ast.Walk(visitor, ctx.Root) 198 | 199 | Expect(result).Should(HaveKeyWithValue("*bytes.Buffer", "WriteString")) 200 | }) 201 | 202 | It("should return the type and call name for package function", func() { 203 | pkg := testutils.NewTestPackage() 204 | defer pkg.Close() 205 | pkg.AddFile("main.go", ` 206 | package main 207 | 208 | import( 209 | "fmt" 210 | ) 211 | 212 | func main() { 213 | fmt.Println("test") 214 | } 215 | `) 216 | ctx := pkg.CreateContext("main.go") 217 | result := map[string]string{} 218 | visitor := testutils.NewMockVisitor() 219 | visitor.Context = ctx 220 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 221 | typeName, call, err := gosec.GetCallInfo(n, ctx) 222 | if err == nil { 223 | result[typeName] = call 224 | } 225 | return true 226 | } 227 | ast.Walk(visitor, ctx.Root) 228 | 229 | Expect(result).Should(HaveKeyWithValue("fmt", "Println")) 230 | }) 231 | }) 232 | Context("when getting binary expression operands", func() { 233 | It("should return all operands of a binary experssion", func() { 234 | pkg := testutils.NewTestPackage() 235 | defer pkg.Close() 236 | pkg.AddFile("main.go", ` 237 | package main 238 | 239 | import( 240 | "fmt" 241 | ) 242 | 243 | func main() { 244 | be := "test1" + "test2" 245 | fmt.Println(be) 246 | } 247 | `) 248 | ctx := pkg.CreateContext("main.go") 249 | var be *ast.BinaryExpr 250 | visitor := testutils.NewMockVisitor() 251 | visitor.Context = ctx 252 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 253 | if expr, ok := n.(*ast.BinaryExpr); ok { 254 | be = expr 255 | } 256 | return true 257 | } 258 | ast.Walk(visitor, ctx.Root) 259 | 260 | operands := gosec.GetBinaryExprOperands(be) 261 | Expect(len(operands)).Should(Equal(2)) 262 | }) 263 | It("should return all operands of complex binary experssion", func() { 264 | pkg := testutils.NewTestPackage() 265 | defer pkg.Close() 266 | pkg.AddFile("main.go", ` 267 | package main 268 | 269 | import( 270 | "fmt" 271 | ) 272 | 273 | func main() { 274 | be := "test1" + "test2" + "test3" + "test4" 275 | fmt.Println(be) 276 | } 277 | `) 278 | ctx := pkg.CreateContext("main.go") 279 | var be *ast.BinaryExpr 280 | visitor := testutils.NewMockVisitor() 281 | visitor.Context = ctx 282 | visitor.Callback = func(n ast.Node, ctx *gosec.Context) bool { 283 | if expr, ok := n.(*ast.BinaryExpr); ok { 284 | if be == nil { 285 | be = expr 286 | } 287 | } 288 | return true 289 | } 290 | ast.Walk(visitor, ctx.Root) 291 | 292 | operands := gosec.GetBinaryExprOperands(be) 293 | Expect(len(operands)).Should(Equal(4)) 294 | }) 295 | }) 296 | }) 297 | -------------------------------------------------------------------------------- /import_tracker.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package gosec 14 | 15 | import ( 16 | "go/ast" 17 | "go/types" 18 | "strings" 19 | ) 20 | 21 | // ImportTracker is used to normalize the packages that have been imported 22 | // by a source file. It is able to differentiate between plain imports, aliased 23 | // imports and init only imports. 24 | type ImportTracker struct { 25 | Imported map[string]string 26 | Aliased map[string]string 27 | InitOnly map[string]bool 28 | } 29 | 30 | // NewImportTracker creates an empty Import tracker instance 31 | func NewImportTracker() *ImportTracker { 32 | return &ImportTracker{ 33 | make(map[string]string), 34 | make(map[string]string), 35 | make(map[string]bool), 36 | } 37 | } 38 | 39 | // TrackFile track all the imports used by the supplied file 40 | func (t *ImportTracker) TrackFile(file *ast.File) { 41 | for _, imp := range file.Imports { 42 | path := strings.Trim(imp.Path.Value, `"`) 43 | parts := strings.Split(path, "/") 44 | if len(parts) > 0 { 45 | name := parts[len(parts)-1] 46 | t.Imported[path] = name 47 | } 48 | } 49 | } 50 | 51 | // TrackPackages tracks all the imports used by the supplied packages 52 | func (t *ImportTracker) TrackPackages(pkgs ...*types.Package) { 53 | for _, pkg := range pkgs { 54 | t.Imported[pkg.Path()] = pkg.Name() 55 | } 56 | } 57 | 58 | // TrackImport tracks imports and handles the 'unsafe' import 59 | func (t *ImportTracker) TrackImport(n ast.Node) { 60 | if imported, ok := n.(*ast.ImportSpec); ok { 61 | path := strings.Trim(imported.Path.Value, `"`) 62 | if imported.Name != nil { 63 | if imported.Name.Name == "_" { 64 | // Initialization only import 65 | t.InitOnly[path] = true 66 | } else { 67 | // Aliased import 68 | t.Aliased[path] = imported.Name.Name 69 | } 70 | } 71 | if path == "unsafe" { 72 | t.Imported[path] = path 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /import_tracker_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "github.com/cosmos/gosec/v2" 5 | "github.com/cosmos/gosec/v2/testutils" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | ) 10 | 11 | var _ = Describe("Import Tracker", func() { 12 | Context("when tracking a file", func() { 13 | It("should parse the imports from file", func() { 14 | tracker := gosec.NewImportTracker() 15 | pkg := testutils.NewTestPackage() 16 | defer pkg.Close() 17 | pkg.AddFile("foo.go", ` 18 | package foo 19 | import "fmt" 20 | func foo() { 21 | fmt.Println() 22 | } 23 | `) 24 | err := pkg.Build() 25 | Expect(err).ShouldNot(HaveOccurred()) 26 | pkgs := pkg.Pkgs() 27 | Expect(pkgs).Should(HaveLen(1)) 28 | files := pkgs[0].Syntax 29 | Expect(files).Should(HaveLen(1)) 30 | tracker.TrackFile(files[0]) 31 | Expect(tracker.Imported).Should(Equal(map[string]string{"fmt": "fmt"})) 32 | }) 33 | It("should parse the named imports from file", func() { 34 | tracker := gosec.NewImportTracker() 35 | pkg := testutils.NewTestPackage() 36 | defer pkg.Close() 37 | pkg.AddFile("foo.go", ` 38 | package foo 39 | import fm "fmt" 40 | func foo() { 41 | fm.Println() 42 | } 43 | `) 44 | err := pkg.Build() 45 | Expect(err).ShouldNot(HaveOccurred()) 46 | pkgs := pkg.Pkgs() 47 | Expect(pkgs).Should(HaveLen(1)) 48 | files := pkgs[0].Syntax 49 | Expect(files).Should(HaveLen(1)) 50 | tracker.TrackFile(files[0]) 51 | Expect(tracker.Imported).Should(Equal(map[string]string{"fmt": "fmt"})) 52 | }) 53 | }) 54 | }) 55 | -------------------------------------------------------------------------------- /issue.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gosec 16 | 17 | import ( 18 | "bufio" 19 | "bytes" 20 | "encoding/json" 21 | "fmt" 22 | "go/ast" 23 | "go/token" 24 | "os" 25 | "strconv" 26 | ) 27 | 28 | // Score type used by severity and confidence values 29 | type Score int 30 | 31 | const ( 32 | // Low severity or confidence 33 | Low Score = iota 34 | // Medium severity or confidence 35 | Medium 36 | // High severity or confidence 37 | High 38 | ) 39 | 40 | // SnippetOffset defines the number of lines captured before 41 | // the beginning and after the end of a code snippet 42 | const SnippetOffset = 1 43 | 44 | // Cwe id and url 45 | type Cwe struct { 46 | ID string 47 | URL string 48 | } 49 | 50 | // GetCwe creates a cwe object for a given RuleID 51 | func GetCwe(id string) Cwe { 52 | return Cwe{ID: id, URL: fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", id)} 53 | } 54 | 55 | // IssueToCWE maps gosec rules to CWEs 56 | var IssueToCWE = map[string]Cwe{ 57 | "G101": GetCwe("798"), 58 | "G102": GetCwe("200"), 59 | "G103": GetCwe("242"), 60 | "G104": GetCwe("703"), 61 | "G106": GetCwe("322"), 62 | "G107": GetCwe("88"), 63 | "G108": GetCwe("200"), 64 | "G109": GetCwe("190"), 65 | "G110": GetCwe("409"), 66 | "G201": GetCwe("89"), 67 | "G202": GetCwe("89"), 68 | "G203": GetCwe("79"), 69 | "G204": GetCwe("78"), 70 | "G301": GetCwe("276"), 71 | "G302": GetCwe("276"), 72 | "G303": GetCwe("377"), 73 | "G304": GetCwe("22"), 74 | "G305": GetCwe("22"), 75 | "G306": GetCwe("276"), 76 | "G307": GetCwe("703"), 77 | "G401": GetCwe("326"), 78 | "G402": GetCwe("295"), 79 | "G403": GetCwe("310"), 80 | "G404": GetCwe("338"), 81 | "G501": GetCwe("327"), 82 | "G502": GetCwe("327"), 83 | "G503": GetCwe("327"), 84 | "G504": GetCwe("327"), 85 | "G505": GetCwe("327"), 86 | "G601": GetCwe("118"), 87 | } 88 | 89 | // Issue is returned by a gosec rule if it discovers an issue with the scanned code. 90 | type Issue struct { 91 | Severity Score `json:"severity"` // issue severity (how problematic it is) 92 | Confidence Score `json:"confidence"` // issue confidence (how sure we are we found it) 93 | Cwe Cwe `json:"cwe"` // Cwe associated with RuleID 94 | RuleID string `json:"rule_id"` // Human readable explanation 95 | What string `json:"details"` // Human readable explanation 96 | File string `json:"file"` // File name we found it in 97 | Code string `json:"code"` // Impacted code line 98 | Line string `json:"line"` // Line number in file 99 | Col string `json:"column"` // Column number in line 100 | } 101 | 102 | // FileLocation point out the file path and line number in file 103 | func (i Issue) FileLocation() string { 104 | return fmt.Sprintf("%s:%s", i.File, i.Line) 105 | } 106 | 107 | // MetaData is embedded in all gosec rules. The Severity, Confidence and What message 108 | // will be passed through to reported issues. 109 | type MetaData struct { 110 | ID string 111 | Severity Score 112 | Confidence Score 113 | What string 114 | } 115 | 116 | // MarshalJSON is used convert a Score object into a JSON representation 117 | func (c Score) MarshalJSON() ([]byte, error) { 118 | return json.Marshal(c.String()) 119 | } 120 | 121 | // String converts a Score into a string 122 | func (c Score) String() string { 123 | switch c { 124 | case High: 125 | return "HIGH" 126 | case Medium: 127 | return "MEDIUM" 128 | case Low: 129 | return "LOW" 130 | } 131 | return "UNDEFINED" 132 | } 133 | 134 | // codeSnippet extracts a code snippet based on the ast reference 135 | func codeSnippet(file *os.File, start int64, end int64, n ast.Node) (string, error) { 136 | if n == nil { 137 | return "", fmt.Errorf("invalid AST node provided") 138 | } 139 | var pos int64 140 | var buf bytes.Buffer 141 | scanner := bufio.NewScanner(file) 142 | scanner.Split(bufio.ScanLines) 143 | for scanner.Scan() { 144 | pos++ 145 | if pos > end { 146 | break 147 | } else if pos >= start && pos <= end { 148 | code := fmt.Sprintf("%d: %s\n", pos, scanner.Text()) 149 | buf.WriteString(code) 150 | } 151 | } 152 | return buf.String(), nil 153 | } 154 | 155 | func codeSnippetStartLine(node ast.Node, fobj *token.File) int64 { 156 | s := (int64)(fobj.Line(node.Pos())) 157 | if s-SnippetOffset > 0 { 158 | return s - SnippetOffset 159 | } 160 | return s 161 | } 162 | 163 | func codeSnippetEndLine(node ast.Node, fobj *token.File) int64 { 164 | e := (int64)(fobj.Line(node.End())) 165 | return e + SnippetOffset 166 | } 167 | 168 | // NewIssue creates a new Issue 169 | func NewIssue(ctx *Context, node ast.Node, ruleID, desc string, severity Score, confidence Score) *Issue { 170 | fobj := ctx.FileSet.File(node.Pos()) 171 | name := fobj.Name() 172 | start, end := fobj.Line(node.Pos()), fobj.Line(node.End()) 173 | line := strconv.Itoa(start) 174 | if start != end { 175 | line = fmt.Sprintf("%d-%d", start, end) 176 | } 177 | col := strconv.Itoa(fobj.Position(node.Pos()).Column) 178 | 179 | var code string 180 | if file, err := os.Open(fobj.Name()); err == nil { 181 | defer file.Close() // #nosec 182 | s := codeSnippetStartLine(node, fobj) 183 | e := codeSnippetEndLine(node, fobj) 184 | code, err = codeSnippet(file, s, e, node) 185 | if err != nil { 186 | code = err.Error() 187 | } 188 | } 189 | 190 | return &Issue{ 191 | File: name, 192 | Line: line, 193 | Col: col, 194 | RuleID: ruleID, 195 | What: desc, 196 | Confidence: confidence, 197 | Severity: severity, 198 | Code: code, 199 | Cwe: IssueToCWE[ruleID], 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /issue_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | "github.com/cosmos/gosec/v2/rules" 8 | "github.com/cosmos/gosec/v2/testutils" 9 | . "github.com/onsi/ginkgo/v2" 10 | . "github.com/onsi/gomega" 11 | ) 12 | 13 | var _ = Describe("Issue", func() { 14 | 15 | Context("when creating a new issue", func() { 16 | It("should create a code snippet from the specified ast.Node", func() { 17 | var target *ast.BasicLit 18 | source := `package main 19 | const foo = "bar" 20 | func main(){ 21 | println(foo) 22 | } 23 | ` 24 | pkg := testutils.NewTestPackage() 25 | defer pkg.Close() 26 | pkg.AddFile("foo.go", source) 27 | ctx := pkg.CreateContext("foo.go") 28 | v := testutils.NewMockVisitor() 29 | v.Callback = func(n ast.Node, ctx *gosec.Context) bool { 30 | if node, ok := n.(*ast.BasicLit); ok { 31 | target = node 32 | return false 33 | } 34 | return true 35 | } 36 | v.Context = ctx 37 | ast.Walk(v, ctx.Root) 38 | Expect(target).ShouldNot(BeNil()) 39 | 40 | issue := gosec.NewIssue(ctx, target, "TEST", "", gosec.High, gosec.High) 41 | Expect(issue).ShouldNot(BeNil()) 42 | Expect(issue.Code).Should(MatchRegexp(`"bar"`)) 43 | Expect(issue.Line).Should(Equal("2")) 44 | Expect(issue.Col).Should(Equal("16")) 45 | Expect(issue.Cwe.ID).Should(Equal("")) 46 | }) 47 | 48 | It("should return an error if specific context is not able to be obtained", func() { 49 | Skip("Not implemented") 50 | }) 51 | 52 | It("should construct file path based on line and file information", func() { 53 | var target *ast.AssignStmt 54 | 55 | source := `package main 56 | import "fmt" 57 | func main() { 58 | username := "admin" 59 | password := "f62e5bcda4fae4f82370da0c6f20697b8f8447ef" 60 | fmt.Println("Doing something with: ", username, password) 61 | }` 62 | 63 | pkg := testutils.NewTestPackage() 64 | defer pkg.Close() 65 | pkg.AddFile("foo.go", source) 66 | ctx := pkg.CreateContext("foo.go") 67 | v := testutils.NewMockVisitor() 68 | v.Callback = func(n ast.Node, ctx *gosec.Context) bool { 69 | if node, ok := n.(*ast.AssignStmt); ok { 70 | if id, ok := node.Lhs[0].(*ast.Ident); ok && id.Name == "password" { 71 | target = node 72 | } 73 | } 74 | return true 75 | } 76 | v.Context = ctx 77 | ast.Walk(v, ctx.Root) 78 | Expect(target).ShouldNot(BeNil()) 79 | 80 | // Use hardcodeded rule to check assignment 81 | cfg := gosec.NewConfig() 82 | rule, _ := rules.NewHardcodedCredentials("TEST", cfg) 83 | issue, err := rule.Match(target, ctx) 84 | Expect(err).ShouldNot(HaveOccurred()) 85 | Expect(issue).ShouldNot(BeNil()) 86 | Expect(issue.FileLocation()).Should(MatchRegexp("foo.go:5")) 87 | }) 88 | 89 | It("should provide accurate line and file information", func() { 90 | Skip("Not implemented") 91 | }) 92 | 93 | It("should provide accurate line and file information for multi-line statements", func() { 94 | var target *ast.CallExpr 95 | source := ` 96 | package main 97 | import ( 98 | "net" 99 | ) 100 | func main() { 101 | _, _ := net.Listen("tcp", 102 | "0.0.0.0:2000") 103 | } 104 | ` 105 | pkg := testutils.NewTestPackage() 106 | defer pkg.Close() 107 | pkg.AddFile("foo.go", source) 108 | ctx := pkg.CreateContext("foo.go") 109 | v := testutils.NewMockVisitor() 110 | v.Callback = func(n ast.Node, ctx *gosec.Context) bool { 111 | if node, ok := n.(*ast.CallExpr); ok { 112 | target = node 113 | } 114 | return true 115 | } 116 | v.Context = ctx 117 | ast.Walk(v, ctx.Root) 118 | Expect(target).ShouldNot(BeNil()) 119 | 120 | cfg := gosec.NewConfig() 121 | rule, _ := rules.NewBindsToAllNetworkInterfaces("TEST", cfg) 122 | issue, err := rule.Match(target, ctx) 123 | Expect(err).ShouldNot(HaveOccurred()) 124 | Expect(issue).ShouldNot(BeNil()) 125 | Expect(issue.File).Should(MatchRegexp("foo.go")) 126 | Expect(issue.Line).Should(MatchRegexp("7-8")) 127 | Expect(issue.Col).Should(Equal("10")) 128 | }) 129 | 130 | It("should maintain the provided severity score", func() { 131 | Skip("Not implemented") 132 | }) 133 | 134 | It("should maintain the provided confidence score", func() { 135 | Skip("Not implemented") 136 | }) 137 | 138 | }) 139 | 140 | }) 141 | -------------------------------------------------------------------------------- /output/formatter_suite_test.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestRules(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Formatters Suite") 13 | } 14 | -------------------------------------------------------------------------------- /output/junit_xml_format.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "encoding/xml" 5 | htmlLib "html" 6 | "strconv" 7 | 8 | "github.com/cosmos/gosec/v2" 9 | ) 10 | 11 | type junitXMLReport struct { 12 | XMLName xml.Name `xml:"testsuites"` 13 | Testsuites []testsuite `xml:"testsuite"` 14 | } 15 | 16 | type testsuite struct { 17 | XMLName xml.Name `xml:"testsuite"` 18 | Name string `xml:"name,attr"` 19 | Tests int `xml:"tests,attr"` 20 | Testcases []testcase `xml:"testcase"` 21 | } 22 | 23 | type testcase struct { 24 | XMLName xml.Name `xml:"testcase"` 25 | Name string `xml:"name,attr"` 26 | Failure failure `xml:"failure"` 27 | } 28 | 29 | type failure struct { 30 | XMLName xml.Name `xml:"failure"` 31 | Message string `xml:"message,attr"` 32 | Text string `xml:",innerxml"` 33 | } 34 | 35 | func generatePlaintext(issue *gosec.Issue) string { 36 | return "Results:\n" + 37 | "[" + issue.File + ":" + issue.Line + "] - " + 38 | issue.What + " (Confidence: " + strconv.Itoa(int(issue.Confidence)) + 39 | ", Severity: " + strconv.Itoa(int(issue.Severity)) + 40 | ", CWE: " + issue.Cwe.ID + ")\n" + "> " + htmlLib.EscapeString(issue.Code) 41 | } 42 | 43 | func createJUnitXMLStruct(data *reportInfo) junitXMLReport { 44 | var xmlReport junitXMLReport 45 | testsuites := map[string]int{} 46 | 47 | for _, issue := range data.Issues { 48 | index, ok := testsuites[issue.What] 49 | if !ok { 50 | xmlReport.Testsuites = append(xmlReport.Testsuites, testsuite{ 51 | Name: issue.What, 52 | }) 53 | index = len(xmlReport.Testsuites) - 1 54 | testsuites[issue.What] = index 55 | } 56 | testcase := testcase{ 57 | Name: issue.File, 58 | Failure: failure{ 59 | Message: "Found 1 vulnerability. See stacktrace for details.", 60 | Text: generatePlaintext(issue), 61 | }, 62 | } 63 | 64 | xmlReport.Testsuites[index].Testcases = append(xmlReport.Testsuites[index].Testcases, testcase) 65 | xmlReport.Testsuites[index].Tests++ 66 | } 67 | 68 | return xmlReport 69 | } 70 | -------------------------------------------------------------------------------- /output/sarif_format.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/cosmos/gosec/v2" 9 | ) 10 | 11 | type sarifLevel string 12 | 13 | const ( 14 | sarifNone = sarifLevel("none") 15 | sarifNote = sarifLevel("note") 16 | sarifWarning = sarifLevel("warning") 17 | sarifError = sarifLevel("error") 18 | ) 19 | 20 | type sarifProperties struct { 21 | Tags []string `json:"tags"` 22 | } 23 | 24 | type sarifRule struct { 25 | ID string `json:"id"` 26 | Name string `json:"name"` 27 | ShortDescription *sarifMessage `json:"shortDescription"` 28 | FullDescription *sarifMessage `json:"fullDescription"` 29 | Help *sarifMessage `json:"help"` 30 | Properties *sarifProperties `json:"properties"` 31 | DefaultConfiguration *sarifConfiguration `json:"defaultConfiguration"` 32 | } 33 | 34 | type sarifConfiguration struct { 35 | Level sarifLevel `json:"level"` 36 | } 37 | 38 | type sarifArtifactLocation struct { 39 | URI string `json:"uri"` 40 | } 41 | 42 | type sarifRegion struct { 43 | StartLine uint64 `json:"startLine"` 44 | EndLine uint64 `json:"endLine"` 45 | StartColumn uint64 `json:"startColumn"` 46 | EndColumn uint64 `json:"endColumn"` 47 | } 48 | 49 | type sarifPhysicalLocation struct { 50 | ArtifactLocation *sarifArtifactLocation `json:"artifactLocation"` 51 | Region *sarifRegion `json:"region"` 52 | } 53 | 54 | type sarifLocation struct { 55 | PhysicalLocation *sarifPhysicalLocation `json:"physicalLocation"` 56 | } 57 | 58 | type sarifMessage struct { 59 | Text string `json:"text"` 60 | } 61 | 62 | type sarifResult struct { 63 | RuleID string `json:"ruleId"` 64 | RuleIndex int `json:"ruleIndex"` 65 | Level sarifLevel `json:"level"` 66 | Message *sarifMessage `json:"message"` 67 | Locations []*sarifLocation `json:"locations"` 68 | } 69 | 70 | type sarifDriver struct { 71 | Name string `json:"name"` 72 | Version string `json:"version"` 73 | InformationURI string `json:"informationUri"` 74 | Rules []*sarifRule `json:"rules,omitempty"` 75 | } 76 | 77 | type sarifTool struct { 78 | Driver *sarifDriver `json:"driver"` 79 | } 80 | 81 | type sarifRun struct { 82 | Tool *sarifTool `json:"tool"` 83 | Results []*sarifResult `json:"results"` 84 | } 85 | 86 | type sarifReport struct { 87 | Schema string `json:"$schema"` 88 | Version string `json:"version"` 89 | Runs []*sarifRun `json:"runs"` 90 | } 91 | 92 | // buildSarifReport return SARIF report struct 93 | func buildSarifReport() *sarifReport { 94 | return &sarifReport{ 95 | Version: "2.1.0", 96 | Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", 97 | Runs: []*sarifRun{}, 98 | } 99 | } 100 | 101 | // buildSarifRule return SARIF rule field struct 102 | func buildSarifRule(issue *gosec.Issue) *sarifRule { 103 | return &sarifRule{ 104 | ID: fmt.Sprintf("%s (CWE-%s)", issue.RuleID, issue.Cwe.ID), 105 | Name: issue.What, 106 | ShortDescription: &sarifMessage{ 107 | Text: issue.What, 108 | }, 109 | FullDescription: &sarifMessage{ 110 | Text: issue.What, 111 | }, 112 | Help: &sarifMessage{ 113 | Text: fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\nCWE: %s", issue.What, issue.Severity.String(), issue.Confidence.String(), issue.Cwe.URL), 114 | }, 115 | Properties: &sarifProperties{ 116 | Tags: []string{fmt.Sprintf("CWE-%s", issue.Cwe.ID), issue.Severity.String()}, 117 | }, 118 | DefaultConfiguration: &sarifConfiguration{ 119 | Level: getSarifLevel(issue.Severity.String()), 120 | }, 121 | } 122 | } 123 | 124 | // buildSarifLocation return SARIF location struct 125 | func buildSarifLocation(issue *gosec.Issue, rootPaths []string) (*sarifLocation, error) { 126 | var filePath string 127 | 128 | lines := strings.Split(issue.Line, "-") 129 | startLine, err := strconv.ParseUint(lines[0], 10, 64) 130 | if err != nil { 131 | return nil, err 132 | } 133 | endLine := startLine 134 | if len(lines) > 1 { 135 | endLine, err = strconv.ParseUint(lines[1], 10, 64) 136 | if err != nil { 137 | return nil, err 138 | } 139 | } 140 | 141 | col, err := strconv.ParseUint(issue.Col, 10, 64) 142 | if err != nil { 143 | return nil, err 144 | } 145 | 146 | for _, rootPath := range rootPaths { 147 | if strings.HasPrefix(issue.File, rootPath) { 148 | filePath = strings.Replace(issue.File, rootPath+"/", "", 1) 149 | } 150 | } 151 | 152 | location := &sarifLocation{ 153 | PhysicalLocation: &sarifPhysicalLocation{ 154 | ArtifactLocation: &sarifArtifactLocation{ 155 | URI: filePath, 156 | }, 157 | Region: &sarifRegion{ 158 | StartLine: startLine, 159 | EndLine: endLine, 160 | StartColumn: col, 161 | EndColumn: col, 162 | }, 163 | }, 164 | } 165 | 166 | return location, nil 167 | } 168 | 169 | // From https://docs.oasis-open.org/sarif/sarif/v2.0/csprd02/sarif-v2.0-csprd02.html#_Toc10127839 170 | // * "warning": The rule specified by ruleId was evaluated and a problem was found. 171 | // * "error": The rule specified by ruleId was evaluated and a serious problem was found. 172 | // * "note": The rule specified by ruleId was evaluated and a minor problem or an opportunity to improve the code was found. 173 | func getSarifLevel(s string) sarifLevel { 174 | switch s { 175 | case "LOW": 176 | return sarifWarning 177 | case "MEDIUM": 178 | return sarifError 179 | case "HIGH": 180 | return sarifError 181 | default: 182 | return sarifNote 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /output/sonarqube_format.go: -------------------------------------------------------------------------------- 1 | package output 2 | 3 | import "github.com/cosmos/gosec/v2" 4 | 5 | type textRange struct { 6 | StartLine int `json:"startLine"` 7 | EndLine int `json:"endLine"` 8 | StartColumn int `json:"startColumn,omitempty"` 9 | EtartColumn int `json:"endColumn,omitempty"` 10 | } 11 | type location struct { 12 | Message string `json:"message"` 13 | FilePath string `json:"filePath"` 14 | TextRange textRange `json:"textRange,omitempty"` 15 | } 16 | 17 | type sonarIssue struct { 18 | EngineID string `json:"engineId"` 19 | RuleID string `json:"ruleId"` 20 | Cwe gosec.Cwe `json:"cwe"` 21 | PrimaryLocation location `json:"primaryLocation"` 22 | Type string `json:"type"` 23 | Severity string `json:"severity"` 24 | EffortMinutes int `json:"effortMinutes"` 25 | SecondaryLocations []location `json:"secondaryLocations,omitempty"` 26 | } 27 | 28 | type sonarIssues struct { 29 | SonarIssues []sonarIssue `json:"issues"` 30 | } 31 | 32 | func getSonarSeverity(s string) string { 33 | switch s { 34 | case "LOW": 35 | return "MINOR" 36 | case "MEDIUM": 37 | return "MAJOR" 38 | case "HIGH": 39 | return "BLOCKER" 40 | default: 41 | return "INFO" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:semverAllMonthly", 4 | ":enableVulnerabilityAlertsWithLabel(vulnerablity)", 5 | ":docker" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /resolve.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package gosec 16 | 17 | import "go/ast" 18 | 19 | func resolveIdent(n *ast.Ident, c *Context) bool { 20 | if n.Obj == nil || n.Obj.Kind != ast.Var { 21 | return true 22 | } 23 | if node, ok := n.Obj.Decl.(ast.Node); ok { 24 | return TryResolve(node, c) 25 | } 26 | return false 27 | } 28 | 29 | func resolveValueSpec(n *ast.ValueSpec, c *Context) bool { 30 | if len(n.Values) == 0 { 31 | return false 32 | } 33 | for _, value := range n.Values { 34 | if !TryResolve(value, c) { 35 | return false 36 | } 37 | } 38 | return true 39 | } 40 | 41 | func resolveAssign(n *ast.AssignStmt, c *Context) bool { 42 | if len(n.Rhs) == 0 { 43 | return false 44 | } 45 | for _, arg := range n.Rhs { 46 | if !TryResolve(arg, c) { 47 | return false 48 | } 49 | } 50 | return true 51 | } 52 | 53 | func resolveCompLit(n *ast.CompositeLit, c *Context) bool { 54 | if len(n.Elts) == 0 { 55 | return false 56 | } 57 | for _, arg := range n.Elts { 58 | if !TryResolve(arg, c) { 59 | return false 60 | } 61 | } 62 | return true 63 | } 64 | 65 | func resolveBinExpr(n *ast.BinaryExpr, c *Context) bool { 66 | return (TryResolve(n.X, c) && TryResolve(n.Y, c)) 67 | } 68 | 69 | func resolveCallExpr(n *ast.CallExpr, c *Context) bool { 70 | // TODO(tkelsey): next step, full function resolution 71 | return false 72 | } 73 | 74 | // TryResolve will attempt, given a subtree starting at some AST node, to resolve 75 | // all values contained within to a known constant. It is used to check for any 76 | // unknown values in compound expressions. 77 | func TryResolve(n ast.Node, c *Context) bool { 78 | switch node := n.(type) { 79 | case *ast.BasicLit: 80 | return true 81 | case *ast.CompositeLit: 82 | return resolveCompLit(node, c) 83 | case *ast.Ident: 84 | return resolveIdent(node, c) 85 | case *ast.ValueSpec: 86 | return resolveValueSpec(node, c) 87 | case *ast.AssignStmt: 88 | return resolveAssign(node, c) 89 | case *ast.CallExpr: 90 | return resolveCallExpr(node, c) 91 | case *ast.BinaryExpr: 92 | return resolveBinExpr(node, c) 93 | } 94 | return false 95 | } 96 | -------------------------------------------------------------------------------- /rule.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package gosec 14 | 15 | import ( 16 | "go/ast" 17 | "reflect" // #nosec G702 18 | ) 19 | 20 | // The Rule interface used by all rules supported by gosec. 21 | type Rule interface { 22 | ID() string 23 | Match(ast.Node, *Context) (*Issue, error) 24 | } 25 | 26 | // RuleBuilder is used to register a rule definition with the analyzer 27 | type RuleBuilder func(id string, c Config) (Rule, []ast.Node) 28 | 29 | // A RuleSet maps lists of rules to the type of AST node they should be run on. 30 | // The analyzer will only invoke rules contained in the list associated with the 31 | // type of AST node it is currently visiting. 32 | type RuleSet map[reflect.Type][]Rule 33 | 34 | // NewRuleSet constructs a new RuleSet 35 | func NewRuleSet() RuleSet { 36 | return make(RuleSet) 37 | } 38 | 39 | // Register adds a trigger for the supplied rule for the the 40 | // specified ast nodes. 41 | func (r RuleSet) Register(rule Rule, nodes ...ast.Node) { 42 | for _, n := range nodes { 43 | t := reflect.TypeOf(n) 44 | if rules, ok := r[t]; ok { 45 | r[t] = append(rules, rule) 46 | } else { 47 | r[t] = []Rule{rule} 48 | } 49 | } 50 | } 51 | 52 | // RegisteredFor will return all rules that are registered for a 53 | // specified ast node. 54 | func (r RuleSet) RegisteredFor(n ast.Node) []Rule { 55 | if rules, found := r[reflect.TypeOf(n)]; found { 56 | return rules 57 | } 58 | return []Rule{} 59 | } 60 | -------------------------------------------------------------------------------- /rule_test.go: -------------------------------------------------------------------------------- 1 | package gosec_test 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | 7 | "github.com/cosmos/gosec/v2" 8 | . "github.com/onsi/ginkgo/v2" 9 | . "github.com/onsi/gomega" 10 | ) 11 | 12 | type mockrule struct { 13 | issue *gosec.Issue 14 | err error 15 | callback func(n ast.Node, ctx *gosec.Context) bool 16 | } 17 | 18 | func (m *mockrule) ID() string { 19 | return "MOCK" 20 | } 21 | 22 | func (m *mockrule) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 23 | if m.callback(n, ctx) { 24 | return m.issue, nil 25 | } 26 | return nil, m.err 27 | } 28 | 29 | var _ = Describe("Rule", func() { 30 | 31 | Context("when using a ruleset", func() { 32 | 33 | var ( 34 | ruleset gosec.RuleSet 35 | dummyErrorRule gosec.Rule 36 | dummyIssueRule gosec.Rule 37 | ) 38 | 39 | JustBeforeEach(func() { 40 | ruleset = gosec.NewRuleSet() 41 | dummyErrorRule = &mockrule{ 42 | issue: nil, 43 | err: fmt.Errorf("An unexpected error occurred"), 44 | callback: func(n ast.Node, ctx *gosec.Context) bool { return false }, 45 | } 46 | dummyIssueRule = &mockrule{ 47 | issue: &gosec.Issue{ 48 | Severity: gosec.High, 49 | Confidence: gosec.High, 50 | What: `Some explanation of the thing`, 51 | File: "main.go", 52 | Code: `#include int main(){ puts("hello world"); }`, 53 | Line: "42", 54 | }, 55 | err: nil, 56 | callback: func(n ast.Node, ctx *gosec.Context) bool { return true }, 57 | } 58 | }) 59 | It("should be possible to register a rule for multiple ast.Node", func() { 60 | registeredNodeA := (*ast.CallExpr)(nil) 61 | registeredNodeB := (*ast.AssignStmt)(nil) 62 | unregisteredNode := (*ast.BinaryExpr)(nil) 63 | 64 | ruleset.Register(dummyIssueRule, registeredNodeA, registeredNodeB) 65 | Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty()) 66 | Expect(ruleset.RegisteredFor(registeredNodeA)).Should(ContainElement(dummyIssueRule)) 67 | Expect(ruleset.RegisteredFor(registeredNodeB)).Should(ContainElement(dummyIssueRule)) 68 | 69 | }) 70 | 71 | It("should not register a rule when no ast.Nodes are specified", func() { 72 | ruleset.Register(dummyErrorRule) 73 | Expect(ruleset).Should(BeEmpty()) 74 | }) 75 | 76 | It("should be possible to retrieve a list of rules for a given node type", func() { 77 | registeredNode := (*ast.CallExpr)(nil) 78 | unregisteredNode := (*ast.AssignStmt)(nil) 79 | ruleset.Register(dummyErrorRule, registeredNode) 80 | ruleset.Register(dummyIssueRule, registeredNode) 81 | Expect(ruleset.RegisteredFor(unregisteredNode)).Should(BeEmpty()) 82 | Expect(ruleset.RegisteredFor(registeredNode)).Should(HaveLen(2)) 83 | Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyErrorRule)) 84 | Expect(ruleset.RegisteredFor(registeredNode)).Should(ContainElement(dummyIssueRule)) 85 | }) 86 | 87 | }) 88 | 89 | }) 90 | -------------------------------------------------------------------------------- /rules/archive.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/cosmos/gosec/v2" 8 | ) 9 | 10 | type archive struct { 11 | gosec.MetaData 12 | calls gosec.CallList 13 | argTypes []string 14 | } 15 | 16 | func (a *archive) ID() string { 17 | return a.MetaData.ID 18 | } 19 | 20 | // Match inspects AST nodes to determine if the filepath.Joins uses any argument derived from type zip.File or tar.Header 21 | func (a *archive) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 22 | if node := a.calls.ContainsPkgCallExpr(n, c, false); node != nil { 23 | for _, arg := range node.Args { 24 | var argType types.Type 25 | if selector, ok := arg.(*ast.SelectorExpr); ok { 26 | argType = c.Info.TypeOf(selector.X) 27 | } else if ident, ok := arg.(*ast.Ident); ok { 28 | if ident.Obj != nil && ident.Obj.Kind == ast.Var { 29 | decl := ident.Obj.Decl 30 | if assign, ok := decl.(*ast.AssignStmt); ok { 31 | if selector, ok := assign.Rhs[0].(*ast.SelectorExpr); ok { 32 | argType = c.Info.TypeOf(selector.X) 33 | } 34 | } 35 | } 36 | } 37 | 38 | if argType != nil { 39 | for _, t := range a.argTypes { 40 | if argType.String() == t { 41 | return gosec.NewIssue(c, n, a.ID(), a.What, a.Severity, a.Confidence), nil 42 | } 43 | } 44 | } 45 | } 46 | } 47 | return nil, nil 48 | } 49 | 50 | // NewArchive creates a new rule which detects the file traversal when extracting zip/tar archives 51 | func NewArchive(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 52 | calls := gosec.NewCallList() 53 | calls.Add("path/filepath", "Join") 54 | calls.Add("path", "Join") 55 | return &archive{ 56 | calls: calls, 57 | argTypes: []string{"*archive/zip.File", "*archive/tar.Header"}, 58 | MetaData: gosec.MetaData{ 59 | ID: id, 60 | Severity: gosec.Medium, 61 | Confidence: gosec.High, 62 | What: "File traversal when extracting zip/tar archive", 63 | }, 64 | }, []ast.Node{(*ast.CallExpr)(nil)} 65 | } 66 | -------------------------------------------------------------------------------- /rules/bad_defer.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "strings" 7 | 8 | "github.com/cosmos/gosec/v2" 9 | ) 10 | 11 | type deferType struct { 12 | typ string 13 | methods []string 14 | } 15 | 16 | type badDefer struct { 17 | gosec.MetaData 18 | types []deferType 19 | } 20 | 21 | func (r *badDefer) ID() string { 22 | return r.MetaData.ID 23 | } 24 | 25 | func normalize(typ string) string { 26 | return strings.TrimPrefix(typ, "*") 27 | } 28 | 29 | func contains(methods []string, method string) bool { 30 | for _, m := range methods { 31 | if m == method { 32 | return true 33 | } 34 | } 35 | return false 36 | } 37 | 38 | func (r *badDefer) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 39 | if deferStmt, ok := n.(*ast.DeferStmt); ok { 40 | for _, deferTyp := range r.types { 41 | if typ, method, err := gosec.GetCallInfo(deferStmt.Call, c); err == nil { 42 | if normalize(typ) == deferTyp.typ && contains(deferTyp.methods, method) { 43 | return gosec.NewIssue(c, n, r.ID(), fmt.Sprintf(r.What, method, typ), r.Severity, r.Confidence), nil 44 | } 45 | } 46 | } 47 | 48 | } 49 | 50 | return nil, nil 51 | } 52 | 53 | // NewDeferredClosing detects unsafe defer of error returning methods 54 | func NewDeferredClosing(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 55 | return &badDefer{ 56 | types: []deferType{ 57 | { 58 | typ: "os.File", 59 | methods: []string{"Close"}, 60 | }, 61 | }, 62 | MetaData: gosec.MetaData{ 63 | ID: id, 64 | Severity: gosec.Medium, 65 | Confidence: gosec.High, 66 | What: "Deferring unsafe method %q on type %q", 67 | }, 68 | }, []ast.Node{(*ast.DeferStmt)(nil)} 69 | } 70 | -------------------------------------------------------------------------------- /rules/bind.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "regexp" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | // Looks for net.Listen("0.0.0.0") or net.Listen(":8080") 25 | type bindsToAllNetworkInterfaces struct { 26 | gosec.MetaData 27 | calls gosec.CallList 28 | pattern *regexp.Regexp 29 | } 30 | 31 | func (r *bindsToAllNetworkInterfaces) ID() string { 32 | return r.MetaData.ID 33 | } 34 | 35 | func (r *bindsToAllNetworkInterfaces) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 36 | callExpr := r.calls.ContainsPkgCallExpr(n, c, false) 37 | if callExpr == nil { 38 | return nil, nil 39 | } 40 | if len(callExpr.Args) > 1 { 41 | arg := callExpr.Args[1] 42 | if bl, ok := arg.(*ast.BasicLit); ok { 43 | if arg, err := gosec.GetString(bl); err == nil { 44 | if r.pattern.MatchString(arg) { 45 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 46 | } 47 | } 48 | } else if ident, ok := arg.(*ast.Ident); ok { 49 | values := gosec.GetIdentStringValues(ident) 50 | for _, value := range values { 51 | if r.pattern.MatchString(value) { 52 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 53 | } 54 | } 55 | } 56 | } else if len(callExpr.Args) > 0 { 57 | values := gosec.GetCallStringArgsValues(callExpr.Args[0], c) 58 | for _, value := range values { 59 | if r.pattern.MatchString(value) { 60 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 61 | } 62 | } 63 | } 64 | return nil, nil 65 | } 66 | 67 | // NewBindsToAllNetworkInterfaces detects socket connections that are setup to 68 | // listen on all network interfaces. 69 | func NewBindsToAllNetworkInterfaces(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 70 | calls := gosec.NewCallList() 71 | calls.Add("net", "Listen") 72 | calls.Add("crypto/tls", "Listen") 73 | return &bindsToAllNetworkInterfaces{ 74 | calls: calls, 75 | pattern: regexp.MustCompile(`^(0.0.0.0|:).*$`), 76 | MetaData: gosec.MetaData{ 77 | ID: id, 78 | Severity: gosec.Medium, 79 | Confidence: gosec.High, 80 | What: "Binds to all network interfaces", 81 | }, 82 | }, []ast.Node{(*ast.CallExpr)(nil)} 83 | } 84 | -------------------------------------------------------------------------------- /rules/blocklist.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "strings" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type blocklistedImport struct { 25 | gosec.MetaData 26 | Blocklisted map[string]string 27 | } 28 | 29 | func unquote(original string) string { 30 | copy := strings.TrimSpace(original) 31 | copy = strings.TrimLeft(copy, `"`) 32 | return strings.TrimRight(copy, `"`) 33 | } 34 | 35 | func (r *blocklistedImport) ID() string { 36 | return r.MetaData.ID 37 | } 38 | 39 | func (r *blocklistedImport) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 40 | if node, ok := n.(*ast.ImportSpec); ok { 41 | if description, ok := r.Blocklisted[unquote(node.Path.Value)]; ok { 42 | return gosec.NewIssue(c, node, r.ID(), description, r.Severity, r.Confidence), nil 43 | } 44 | } 45 | return nil, nil 46 | } 47 | 48 | // NewBlocklistedImports reports when a blocklisted import is being used. 49 | // Typically when a deprecated technology is being used. 50 | func NewBlocklistedImports(id string, conf gosec.Config, blocklist map[string]string) (gosec.Rule, []ast.Node) { 51 | return &blocklistedImport{ 52 | MetaData: gosec.MetaData{ 53 | ID: id, 54 | Severity: gosec.Medium, 55 | Confidence: gosec.High, 56 | }, 57 | Blocklisted: blocklist, 58 | }, []ast.Node{(*ast.ImportSpec)(nil)} 59 | } 60 | 61 | // NewBlocklistedImportMD5 fails if MD5 is imported 62 | func NewBlocklistedImportMD5(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 63 | return NewBlocklistedImports(id, conf, map[string]string{ 64 | "crypto/md5": "Blocklisted import crypto/md5: weak cryptographic primitive", 65 | }) 66 | } 67 | 68 | // NewBlocklistedImportDES fails if DES is imported 69 | func NewBlocklistedImportDES(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 70 | return NewBlocklistedImports(id, conf, map[string]string{ 71 | "crypto/des": "Blocklisted import crypto/des: weak cryptographic primitive", 72 | }) 73 | } 74 | 75 | // NewBlocklistedImportRC4 fails if DES is imported 76 | func NewBlocklistedImportRC4(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 77 | return NewBlocklistedImports(id, conf, map[string]string{ 78 | "crypto/rc4": "Blocklisted import crypto/rc4: weak cryptographic primitive", 79 | }) 80 | } 81 | 82 | // NewBlocklistedImportCGI fails if CGI is imported 83 | func NewBlocklistedImportCGI(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 84 | return NewBlocklistedImports(id, conf, map[string]string{ 85 | "net/http/cgi": "Blocklisted import net/http/cgi: Go versions < 1.6.3 are vulnerable to Httpoxy attack: (CVE-2016-5386)", 86 | }) 87 | } 88 | 89 | // NewBlocklistedImportSHA1 fails if SHA1 is imported 90 | func NewBlocklistedImportSHA1(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 91 | return NewBlocklistedImports(id, conf, map[string]string{ 92 | "crypto/sha1": "Blocklisted import crypto/sha1: weak cryptographic primitive", 93 | }) 94 | } 95 | -------------------------------------------------------------------------------- /rules/decompression-bomb.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "fmt" 19 | "go/ast" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type decompressionBombCheck struct { 25 | gosec.MetaData 26 | readerCalls gosec.CallList 27 | copyCalls gosec.CallList 28 | } 29 | 30 | func (d *decompressionBombCheck) ID() string { 31 | return d.MetaData.ID 32 | } 33 | 34 | func containsReaderCall(node ast.Node, ctx *gosec.Context, list gosec.CallList) bool { 35 | if list.ContainsPkgCallExpr(node, ctx, false) != nil { 36 | return true 37 | } 38 | // Resolve type info of ident (for *archive/zip.File.Open) 39 | /* #nosec G703 */ 40 | s, idt, _ := gosec.GetCallInfo(node, ctx) 41 | return list.Contains(s, idt) 42 | } 43 | 44 | func (d *decompressionBombCheck) Match(node ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 45 | var readerVarObj map[*ast.Object]struct{} 46 | 47 | // To check multiple lines, ctx.PassedValues is used to store temporary data. 48 | if _, ok := ctx.PassedValues[d.ID()]; !ok { 49 | readerVarObj = make(map[*ast.Object]struct{}) 50 | ctx.PassedValues[d.ID()] = readerVarObj 51 | } else if pv, ok := ctx.PassedValues[d.ID()].(map[*ast.Object]struct{}); ok { 52 | readerVarObj = pv 53 | } else { 54 | return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*ast.Object]struct{}, but %T", d.ID(), ctx.PassedValues[d.ID()]) 55 | } 56 | 57 | // io.Copy is a common function. 58 | // To reduce false positives, This rule detects code which is used for compressed data only. 59 | switch n := node.(type) { 60 | case *ast.AssignStmt: 61 | for _, expr := range n.Rhs { 62 | if callExpr, ok := expr.(*ast.CallExpr); ok && containsReaderCall(callExpr, ctx, d.readerCalls) { 63 | if idt, ok := n.Lhs[0].(*ast.Ident); ok && idt.Name != "_" { 64 | // Example: 65 | // r, _ := zlib.NewReader(buf) 66 | // Add r's Obj to readerVarObj map 67 | readerVarObj[idt.Obj] = struct{}{} 68 | } 69 | } 70 | } 71 | case *ast.CallExpr: 72 | if d.copyCalls.ContainsPkgCallExpr(n, ctx, false) != nil { 73 | if idt, ok := n.Args[1].(*ast.Ident); ok { 74 | if _, ok := readerVarObj[idt.Obj]; ok { 75 | // Detect io.Copy(x, r) 76 | return gosec.NewIssue(ctx, n, d.ID(), d.What, d.Severity, d.Confidence), nil 77 | } 78 | } 79 | } 80 | } 81 | 82 | return nil, nil 83 | } 84 | 85 | // NewDecompressionBombCheck detects if there is potential DoS vulnerability via decompression bomb 86 | func NewDecompressionBombCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 87 | readerCalls := gosec.NewCallList() 88 | readerCalls.Add("compress/gzip", "NewReader") 89 | readerCalls.AddAll("compress/zlib", "NewReader", "NewReaderDict") 90 | readerCalls.Add("compress/bzip2", "NewReader") 91 | readerCalls.AddAll("compress/flate", "NewReader", "NewReaderDict") 92 | readerCalls.Add("compress/lzw", "NewReader") 93 | readerCalls.Add("archive/tar", "NewReader") 94 | readerCalls.Add("archive/zip", "NewReader") 95 | readerCalls.Add("*archive/zip.File", "Open") 96 | 97 | copyCalls := gosec.NewCallList() 98 | copyCalls.Add("io", "Copy") 99 | copyCalls.Add("io", "CopyBuffer") 100 | 101 | return &decompressionBombCheck{ 102 | MetaData: gosec.MetaData{ 103 | ID: id, 104 | Severity: gosec.Medium, 105 | Confidence: gosec.Medium, 106 | What: "Potential DoS vulnerability via decompression bomb", 107 | }, 108 | readerCalls: readerCalls, 109 | copyCalls: copyCalls, 110 | }, []ast.Node{(*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)} 111 | } 112 | -------------------------------------------------------------------------------- /rules/errors.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "go/types" 20 | "sort" 21 | 22 | "github.com/cosmos/gosec/v2" 23 | ) 24 | 25 | type noErrorCheck struct { 26 | gosec.MetaData 27 | whitelist gosec.CallList 28 | } 29 | 30 | func (r *noErrorCheck) ID() string { 31 | return r.MetaData.ID 32 | } 33 | 34 | func returnsError(callExpr *ast.CallExpr, ctx *gosec.Context) int { 35 | if tv := ctx.Info.TypeOf(callExpr); tv != nil { 36 | switch t := tv.(type) { 37 | case *types.Tuple: 38 | for pos := 0; pos < t.Len(); pos++ { 39 | variable := t.At(pos) 40 | if variable != nil && variable.Type().String() == "error" { 41 | return pos 42 | } 43 | } 44 | case *types.Named: 45 | if t.String() == "error" { 46 | return 0 47 | } 48 | } 49 | } 50 | return -1 51 | } 52 | 53 | func (r *noErrorCheck) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 54 | switch stmt := n.(type) { 55 | case *ast.AssignStmt: 56 | cfg := ctx.Config 57 | if enabled, err := cfg.IsGlobalEnabled(gosec.Audit); err == nil && enabled { 58 | for _, expr := range stmt.Rhs { 59 | if callExpr, ok := expr.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(expr, ctx) == nil { 60 | pos := returnsError(callExpr, ctx) 61 | if pos < 0 || pos >= len(stmt.Lhs) { 62 | return nil, nil 63 | } 64 | if id, ok := stmt.Lhs[pos].(*ast.Ident); ok && id.Name == "_" { 65 | return gosec.NewIssue(ctx, n, r.ID(), r.What, r.Severity, r.Confidence), nil 66 | } 67 | } 68 | } 69 | } 70 | case *ast.ExprStmt: 71 | if callExpr, ok := stmt.X.(*ast.CallExpr); ok && r.whitelist.ContainsCallExpr(stmt.X, ctx) == nil { 72 | pos := returnsError(callExpr, ctx) 73 | if pos >= 0 { 74 | return gosec.NewIssue(ctx, n, r.ID(), r.What, r.Severity, r.Confidence), nil 75 | } 76 | } 77 | } 78 | return nil, nil 79 | } 80 | 81 | // NewNoErrorCheck detects if the returned error is unchecked 82 | func NewNoErrorCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 83 | // TODO(gm) Come up with sensible defaults here. Or flip it to use a 84 | // black list instead. 85 | whitelist := gosec.NewCallList() 86 | whitelist.AddAll("bytes.Buffer", "Write", "WriteByte", "WriteRune", "WriteString") 87 | whitelist.AddAll("fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") 88 | whitelist.AddAll("strings.Builder", "Write", "WriteByte", "WriteRune", "WriteString") 89 | whitelist.Add("io.PipeWriter", "CloseWithError") 90 | whitelist.Add("hash.Hash", "Write") 91 | 92 | if configured, ok := conf["G104"]; ok { 93 | if whitelisted, ok := configured.(map[string]interface{}); ok { 94 | pkgs := make([]string, 0, len(whitelisted)) 95 | for pkg := range whitelisted { 96 | pkgs = append(pkgs, pkg) 97 | } 98 | sort.Slice(pkgs, func(i, j int) bool { return pkgs[i] < pkgs[j] }) 99 | for _, pkg := range pkgs { 100 | if funcs, ok := whitelisted[pkg].([]interface{}); ok { 101 | whitelist.AddAll(pkg, toStringSlice(funcs)...) 102 | } 103 | } 104 | } 105 | } 106 | 107 | return &noErrorCheck{ 108 | MetaData: gosec.MetaData{ 109 | ID: id, 110 | Severity: gosec.Low, 111 | Confidence: gosec.High, 112 | What: "Errors unhandled.", 113 | }, 114 | whitelist: whitelist, 115 | }, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)} 116 | } 117 | 118 | func toStringSlice(values []interface{}) []string { 119 | result := []string{} 120 | for _, value := range values { 121 | if value, ok := value.(string); ok { 122 | result = append(result, value) 123 | } 124 | } 125 | return result 126 | } 127 | -------------------------------------------------------------------------------- /rules/fileperms.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "fmt" 19 | "go/ast" 20 | "strconv" 21 | 22 | "github.com/cosmos/gosec/v2" 23 | ) 24 | 25 | type filePermissions struct { 26 | gosec.MetaData 27 | mode int64 28 | pkg string 29 | calls []string 30 | } 31 | 32 | func (r *filePermissions) ID() string { 33 | return r.MetaData.ID 34 | } 35 | 36 | func getConfiguredMode(conf map[string]interface{}, configKey string, defaultMode int64) int64 { 37 | var mode = defaultMode 38 | if value, ok := conf[configKey]; ok { 39 | switch value := value.(type) { 40 | case int64: 41 | mode = value 42 | case string: 43 | if m, e := strconv.ParseInt(value, 0, 64); e != nil { 44 | mode = defaultMode 45 | } else { 46 | mode = m 47 | } 48 | } 49 | } 50 | return mode 51 | } 52 | 53 | func (r *filePermissions) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 54 | if callexpr, matched := gosec.MatchCallByPackage(n, c, r.pkg, r.calls...); matched { 55 | modeArg := callexpr.Args[len(callexpr.Args)-1] 56 | if mode, err := gosec.GetInt(modeArg); err == nil && mode > r.mode { 57 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 58 | } 59 | } 60 | return nil, nil 61 | } 62 | 63 | // NewWritePerms creates a rule to detect file Writes with bad permissions. 64 | func NewWritePerms(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 65 | mode := getConfiguredMode(conf, "G306", 0600) 66 | return &filePermissions{ 67 | mode: mode, 68 | pkg: "io/ioutil", 69 | calls: []string{"WriteFile"}, 70 | MetaData: gosec.MetaData{ 71 | ID: id, 72 | Severity: gosec.Medium, 73 | Confidence: gosec.High, 74 | What: fmt.Sprintf("Expect WriteFile permissions to be %#o or less", mode), 75 | }, 76 | }, []ast.Node{(*ast.CallExpr)(nil)} 77 | } 78 | 79 | // NewFilePerms creates a rule to detect file creation with a more permissive than configured 80 | // permission mask. 81 | func NewFilePerms(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 82 | mode := getConfiguredMode(conf, "G302", 0600) 83 | return &filePermissions{ 84 | mode: mode, 85 | pkg: "os", 86 | calls: []string{"OpenFile", "Chmod"}, 87 | MetaData: gosec.MetaData{ 88 | ID: id, 89 | Severity: gosec.Medium, 90 | Confidence: gosec.High, 91 | What: fmt.Sprintf("Expect file permissions to be %#o or less", mode), 92 | }, 93 | }, []ast.Node{(*ast.CallExpr)(nil)} 94 | } 95 | 96 | // NewMkdirPerms creates a rule to detect directory creation with more permissive than 97 | // configured permission mask. 98 | func NewMkdirPerms(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 99 | mode := getConfiguredMode(conf, "G301", 0750) 100 | return &filePermissions{ 101 | mode: mode, 102 | pkg: "os", 103 | calls: []string{"Mkdir", "MkdirAll"}, 104 | MetaData: gosec.MetaData{ 105 | ID: id, 106 | Severity: gosec.Medium, 107 | Confidence: gosec.High, 108 | What: fmt.Sprintf("Expect directory permissions to be %#o or less", mode), 109 | }, 110 | }, []ast.Node{(*ast.CallExpr)(nil)} 111 | } 112 | -------------------------------------------------------------------------------- /rules/hardcoded_credentials.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "go/token" 20 | "regexp" 21 | "strconv" 22 | 23 | "github.com/cosmos/gosec/v2" 24 | zxcvbn "github.com/nbutton23/zxcvbn-go" 25 | ) 26 | 27 | type credentials struct { 28 | gosec.MetaData 29 | pattern *regexp.Regexp 30 | entropyThreshold float64 31 | perCharThreshold float64 32 | truncate int 33 | ignoreEntropy bool 34 | } 35 | 36 | func (r *credentials) ID() string { 37 | return r.MetaData.ID 38 | } 39 | 40 | func truncate(s string, n int) string { 41 | if n > len(s) { 42 | return s 43 | } 44 | return s[:n] 45 | } 46 | 47 | func (r *credentials) isHighEntropyString(str string) bool { 48 | s := truncate(str, r.truncate) 49 | info := zxcvbn.PasswordStrength(s, []string{}) 50 | entropyPerChar := info.Entropy / float64(len(s)) 51 | return (info.Entropy >= r.entropyThreshold || 52 | (info.Entropy >= (r.entropyThreshold/2) && 53 | entropyPerChar >= r.perCharThreshold)) 54 | } 55 | 56 | func (r *credentials) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 57 | switch node := n.(type) { 58 | case *ast.AssignStmt: 59 | return r.matchAssign(node, ctx) 60 | case *ast.ValueSpec: 61 | return r.matchValueSpec(node, ctx) 62 | case *ast.BinaryExpr: 63 | return r.matchEqualityCheck(node, ctx) 64 | } 65 | return nil, nil 66 | } 67 | 68 | func (r *credentials) matchAssign(assign *ast.AssignStmt, ctx *gosec.Context) (*gosec.Issue, error) { 69 | for _, i := range assign.Lhs { 70 | if ident, ok := i.(*ast.Ident); ok { 71 | if r.pattern.MatchString(ident.Name) { 72 | for _, e := range assign.Rhs { 73 | if val, err := gosec.GetString(e); err == nil { 74 | if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) { 75 | return gosec.NewIssue(ctx, assign, r.ID(), r.What, r.Severity, r.Confidence), nil 76 | } 77 | } 78 | } 79 | } 80 | } 81 | } 82 | return nil, nil 83 | } 84 | 85 | func (r *credentials) matchValueSpec(valueSpec *ast.ValueSpec, ctx *gosec.Context) (*gosec.Issue, error) { 86 | for index, ident := range valueSpec.Names { 87 | if r.pattern.MatchString(ident.Name) && valueSpec.Values != nil { 88 | // const foo, bar = "same value" 89 | if len(valueSpec.Values) <= index { 90 | index = len(valueSpec.Values) - 1 91 | } 92 | if val, err := gosec.GetString(valueSpec.Values[index]); err == nil { 93 | if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) { 94 | return gosec.NewIssue(ctx, valueSpec, r.ID(), r.What, r.Severity, r.Confidence), nil 95 | } 96 | } 97 | } 98 | } 99 | return nil, nil 100 | } 101 | 102 | func (r *credentials) matchEqualityCheck(binaryExpr *ast.BinaryExpr, ctx *gosec.Context) (*gosec.Issue, error) { 103 | if binaryExpr.Op == token.EQL || binaryExpr.Op == token.NEQ { 104 | if ident, ok := binaryExpr.X.(*ast.Ident); ok { 105 | if r.pattern.MatchString(ident.Name) { 106 | if val, err := gosec.GetString(binaryExpr.Y); err == nil { 107 | if r.ignoreEntropy || (!r.ignoreEntropy && r.isHighEntropyString(val)) { 108 | return gosec.NewIssue(ctx, binaryExpr, r.ID(), r.What, r.Severity, r.Confidence), nil 109 | } 110 | } 111 | } 112 | } 113 | } 114 | return nil, nil 115 | } 116 | 117 | // NewHardcodedCredentials attempts to find high entropy string constants being 118 | // assigned to variables that appear to be related to credentials. 119 | func NewHardcodedCredentials(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 120 | pattern := `(?i)passwd|pass|password|pwd|secret|token` 121 | entropyThreshold := 80.0 122 | perCharThreshold := 3.0 123 | ignoreEntropy := false 124 | var truncateString = 16 125 | if val, ok := conf["G101"]; ok { 126 | conf := val.(map[string]interface{}) 127 | if configPattern, ok := conf["pattern"]; ok { 128 | if cfgPattern, ok := configPattern.(string); ok { 129 | pattern = cfgPattern 130 | } 131 | } 132 | if configIgnoreEntropy, ok := conf["ignore_entropy"]; ok { 133 | if cfgIgnoreEntropy, ok := configIgnoreEntropy.(bool); ok { 134 | ignoreEntropy = cfgIgnoreEntropy 135 | } 136 | } 137 | if configEntropyThreshold, ok := conf["entropy_threshold"]; ok { 138 | if cfgEntropyThreshold, ok := configEntropyThreshold.(string); ok { 139 | if parsedNum, err := strconv.ParseFloat(cfgEntropyThreshold, 64); err == nil { 140 | entropyThreshold = parsedNum 141 | } 142 | } 143 | } 144 | if configCharThreshold, ok := conf["per_char_threshold"]; ok { 145 | if cfgCharThreshold, ok := configCharThreshold.(string); ok { 146 | if parsedNum, err := strconv.ParseFloat(cfgCharThreshold, 64); err == nil { 147 | perCharThreshold = parsedNum 148 | } 149 | } 150 | } 151 | if configTruncate, ok := conf["truncate"]; ok { 152 | if cfgTruncate, ok := configTruncate.(string); ok { 153 | if parsedInt, err := strconv.Atoi(cfgTruncate); err == nil { 154 | truncateString = parsedInt 155 | } 156 | } 157 | } 158 | } 159 | 160 | return &credentials{ 161 | pattern: regexp.MustCompile(pattern), 162 | entropyThreshold: entropyThreshold, 163 | perCharThreshold: perCharThreshold, 164 | ignoreEntropy: ignoreEntropy, 165 | truncate: truncateString, 166 | MetaData: gosec.MetaData{ 167 | ID: id, 168 | What: "Potential hardcoded credentials", 169 | Confidence: gosec.Low, 170 | Severity: gosec.High, 171 | }, 172 | }, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ValueSpec)(nil), (*ast.BinaryExpr)(nil)} 173 | } 174 | -------------------------------------------------------------------------------- /rules/implicit_aliasing.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | "go/token" 6 | 7 | "github.com/cosmos/gosec/v2" 8 | ) 9 | 10 | type implicitAliasing struct { 11 | gosec.MetaData 12 | aliases map[*ast.Object]struct{} 13 | rightBrace token.Pos 14 | acceptableAlias []*ast.UnaryExpr 15 | } 16 | 17 | func (r *implicitAliasing) ID() string { 18 | return r.MetaData.ID 19 | } 20 | 21 | func containsUnary(exprs []*ast.UnaryExpr, expr *ast.UnaryExpr) bool { 22 | for _, e := range exprs { 23 | if e == expr { 24 | return true 25 | } 26 | } 27 | return false 28 | } 29 | 30 | func (r *implicitAliasing) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 31 | switch node := n.(type) { 32 | case *ast.RangeStmt: 33 | // When presented with a range statement, get the underlying Object bound to 34 | // by assignment and add it to our set (r.aliases) of objects to check for. 35 | if key, ok := node.Value.(*ast.Ident); ok { 36 | if key.Obj != nil { 37 | if assignment, ok := key.Obj.Decl.(*ast.AssignStmt); ok { 38 | if len(assignment.Lhs) < 2 { 39 | return nil, nil 40 | } 41 | 42 | if object, ok := assignment.Lhs[1].(*ast.Ident); ok { 43 | r.aliases[object.Obj] = struct{}{} 44 | 45 | if r.rightBrace < node.Body.Rbrace { 46 | r.rightBrace = node.Body.Rbrace 47 | } 48 | } 49 | } 50 | } 51 | } 52 | 53 | case *ast.UnaryExpr: 54 | // If this unary expression is outside of the last range statement we were looking at 55 | // then clear the list of objects we're concerned about because they're no longer in 56 | // scope 57 | if node.Pos() > r.rightBrace { 58 | r.aliases = make(map[*ast.Object]struct{}) 59 | r.acceptableAlias = make([]*ast.UnaryExpr, 0) 60 | } 61 | 62 | // Short circuit logic to skip checking aliases if we have nothing to check against. 63 | if len(r.aliases) == 0 { 64 | return nil, nil 65 | } 66 | 67 | // If this unary is at the top level of a return statement then it is okay-- 68 | // see *ast.ReturnStmt comment below. 69 | if containsUnary(r.acceptableAlias, node) { 70 | return nil, nil 71 | } 72 | 73 | // If we find a unary op of & (reference) of an object within r.aliases, complain. 74 | if ident, ok := node.X.(*ast.Ident); ok && node.Op.String() == "&" { 75 | if _, contains := r.aliases[ident.Obj]; contains { 76 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 77 | } 78 | } 79 | case *ast.ReturnStmt: 80 | // Returning a rangeStmt yielded value is acceptable since only one value will be returned 81 | for _, item := range node.Results { 82 | if unary, ok := item.(*ast.UnaryExpr); ok && unary.Op.String() == "&" { 83 | r.acceptableAlias = append(r.acceptableAlias, unary) 84 | } 85 | } 86 | } 87 | 88 | return nil, nil 89 | } 90 | 91 | // NewImplicitAliasing detects implicit memory aliasing of type: for blah := SomeCall() {... SomeOtherCall(&blah) ...} 92 | func NewImplicitAliasing(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 93 | return &implicitAliasing{ 94 | aliases: make(map[*ast.Object]struct{}), 95 | rightBrace: token.NoPos, 96 | acceptableAlias: make([]*ast.UnaryExpr, 0), 97 | MetaData: gosec.MetaData{ 98 | ID: id, 99 | Severity: gosec.Medium, 100 | Confidence: gosec.Medium, 101 | What: "Implicit memory aliasing in for loop.", 102 | }, 103 | }, []ast.Node{(*ast.RangeStmt)(nil), (*ast.UnaryExpr)(nil), (*ast.ReturnStmt)(nil)} 104 | } 105 | 106 | /* 107 | This rule is prone to flag false positives. 108 | 109 | Within GoSec, the rule is just an AST match-- there are a handful of other 110 | implementation strategies which might lend more nuance to the rule at the 111 | cost of allowing false negatives. 112 | 113 | From a tooling side, I'd rather have this rule flag false positives than 114 | potentially have some false negatives-- especially if the sentiment of this 115 | rule (as I understand it, and Go) is that referencing a rangeStmt-yielded 116 | value is kinda strange and does not have a strongly justified use case. 117 | 118 | Which is to say-- a false positive _should_ just be changed. 119 | */ 120 | -------------------------------------------------------------------------------- /rules/integer_overflow.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "fmt" 19 | "go/ast" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type integerOverflowCheck struct { 25 | gosec.MetaData 26 | calls gosec.CallList 27 | } 28 | 29 | func (i *integerOverflowCheck) ID() string { 30 | return i.MetaData.ID 31 | } 32 | 33 | func (i *integerOverflowCheck) Match(node ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 34 | var atoiVarObj map[*ast.Object]ast.Node 35 | 36 | // To check multiple lines, ctx.PassedValues is used to store temporary data. 37 | if _, ok := ctx.PassedValues[i.ID()]; !ok { 38 | atoiVarObj = make(map[*ast.Object]ast.Node) 39 | ctx.PassedValues[i.ID()] = atoiVarObj 40 | } else if pv, ok := ctx.PassedValues[i.ID()].(map[*ast.Object]ast.Node); ok { 41 | atoiVarObj = pv 42 | } else { 43 | return nil, fmt.Errorf("PassedValues[%s] of Context is not map[*ast.Object]ast.Node, but %T", i.ID(), ctx.PassedValues[i.ID()]) 44 | } 45 | 46 | // strconv.Atoi is a common function. 47 | // To reduce false positives, This rule detects code which is converted to int32/int16 only. 48 | switch n := node.(type) { 49 | case *ast.AssignStmt: 50 | for _, expr := range n.Rhs { 51 | if callExpr, ok := expr.(*ast.CallExpr); ok && i.calls.ContainsPkgCallExpr(callExpr, ctx, false) != nil { 52 | if idt, ok := n.Lhs[0].(*ast.Ident); ok && idt.Name != "_" { 53 | // Example: 54 | // v, _ := strconv.Atoi("1111") 55 | // Add v's Obj to atoiVarObj map 56 | atoiVarObj[idt.Obj] = n 57 | } 58 | } 59 | } 60 | case *ast.CallExpr: 61 | if fun, ok := n.Fun.(*ast.Ident); ok { 62 | if fun.Name == "int32" || fun.Name == "int16" { 63 | if idt, ok := n.Args[0].(*ast.Ident); ok { 64 | if n, ok := atoiVarObj[idt.Obj]; ok { 65 | // Detect int32(v) and int16(v) 66 | return gosec.NewIssue(ctx, n, i.ID(), i.What, i.Severity, i.Confidence), nil 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | return nil, nil 74 | } 75 | 76 | // NewIntegerOverflowCheck detects if there is potential Integer OverFlow 77 | func NewIntegerOverflowCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 78 | calls := gosec.NewCallList() 79 | calls.Add("strconv", "Atoi") 80 | return &integerOverflowCheck{ 81 | MetaData: gosec.MetaData{ 82 | ID: id, 83 | Severity: gosec.High, 84 | Confidence: gosec.Medium, 85 | What: "Potential Integer overflow made by strconv.Atoi result conversion to int16/32", 86 | }, 87 | calls: calls, 88 | }, []ast.Node{(*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)} 89 | } 90 | -------------------------------------------------------------------------------- /rules/pprof.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | ) 8 | 9 | type pprofCheck struct { 10 | gosec.MetaData 11 | importPath string 12 | importName string 13 | } 14 | 15 | // ID returns the ID of the check 16 | func (p *pprofCheck) ID() string { 17 | return p.MetaData.ID 18 | } 19 | 20 | // Match checks for pprof imports 21 | func (p *pprofCheck) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 22 | if node, ok := n.(*ast.ImportSpec); ok { 23 | if p.importPath == unquote(node.Path.Value) && node.Name != nil && p.importName == node.Name.Name { 24 | return gosec.NewIssue(c, node, p.ID(), p.What, p.Severity, p.Confidence), nil 25 | } 26 | } 27 | return nil, nil 28 | } 29 | 30 | // NewPprofCheck detects when the profiling endpoint is automatically exposed 31 | func NewPprofCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 32 | return &pprofCheck{ 33 | MetaData: gosec.MetaData{ 34 | ID: id, 35 | Severity: gosec.High, 36 | Confidence: gosec.High, 37 | What: "Profiling endpoint is automatically exposed on /debug/pprof", 38 | }, 39 | importPath: "net/http/pprof", 40 | importName: "_", 41 | }, []ast.Node{(*ast.ImportSpec)(nil)} 42 | } 43 | -------------------------------------------------------------------------------- /rules/rand.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | 20 | "github.com/cosmos/gosec/v2" 21 | ) 22 | 23 | type weakRand struct { 24 | gosec.MetaData 25 | funcNames []string 26 | packagePath string 27 | } 28 | 29 | func (w *weakRand) ID() string { 30 | return w.MetaData.ID 31 | } 32 | 33 | func (w *weakRand) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 34 | for _, funcName := range w.funcNames { 35 | if _, matched := gosec.MatchCallByPackage(n, c, w.packagePath, funcName); matched { 36 | return gosec.NewIssue(c, n, w.ID(), w.What, w.Severity, w.Confidence), nil 37 | } 38 | } 39 | 40 | return nil, nil 41 | } 42 | 43 | // NewWeakRandCheck detects the use of random number generator that isn't cryptographically secure 44 | func NewWeakRandCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 45 | return &weakRand{ 46 | funcNames: []string{"New", "Read", "Float32", "Float64", "Int", "Int31", 47 | "Int31n", "Int63", "Int63n", "Intn", "NormalFloat64", "Uint32", "Uint64"}, 48 | packagePath: "math/rand", 49 | MetaData: gosec.MetaData{ 50 | ID: id, 51 | Severity: gosec.High, 52 | Confidence: gosec.Medium, 53 | What: "Use of weak random number generator (math/rand instead of crypto/rand)", 54 | }, 55 | }, []ast.Node{(*ast.CallExpr)(nil)} 56 | } 57 | -------------------------------------------------------------------------------- /rules/readfile.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "go/types" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type readfile struct { 25 | gosec.MetaData 26 | gosec.CallList 27 | pathJoin gosec.CallList 28 | clean gosec.CallList 29 | } 30 | 31 | // ID returns the identifier for this rule 32 | func (r *readfile) ID() string { 33 | return r.MetaData.ID 34 | } 35 | 36 | // isJoinFunc checks if there is a filepath.Join or other join function 37 | func (r *readfile) isJoinFunc(n ast.Node, c *gosec.Context) bool { 38 | if call := r.pathJoin.ContainsPkgCallExpr(n, c, false); call != nil { 39 | for _, arg := range call.Args { 40 | // edge case: check if one of the args is a BinaryExpr 41 | if binExp, ok := arg.(*ast.BinaryExpr); ok { 42 | // iterate and resolve all found identities from the BinaryExpr 43 | if _, ok := gosec.FindVarIdentities(binExp, c); ok { 44 | return true 45 | } 46 | } 47 | 48 | // try and resolve identity 49 | if ident, ok := arg.(*ast.Ident); ok { 50 | obj := c.Info.ObjectOf(ident) 51 | if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) { 52 | return true 53 | } 54 | } 55 | } 56 | } 57 | return false 58 | } 59 | 60 | // isFilepathClean checks if there is a filepath.Clean before assigning to a variable 61 | func (r *readfile) isFilepathClean(n *ast.Ident, c *gosec.Context) bool { 62 | if n.Obj.Kind != ast.Var { 63 | return false 64 | } 65 | if node, ok := n.Obj.Decl.(*ast.AssignStmt); ok { 66 | if call, ok := node.Rhs[0].(*ast.CallExpr); ok { 67 | if clean := r.clean.ContainsPkgCallExpr(call, c, false); clean != nil { 68 | return true 69 | } 70 | } 71 | } 72 | return false 73 | } 74 | 75 | // Match inspects AST nodes to determine if the match the methods `os.Open` or `ioutil.ReadFile` 76 | func (r *readfile) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 77 | if node := r.ContainsPkgCallExpr(n, c, false); node != nil { 78 | for _, arg := range node.Args { 79 | // handles path joining functions in Arg 80 | // eg. os.Open(filepath.Join("/tmp/", file)) 81 | if callExpr, ok := arg.(*ast.CallExpr); ok { 82 | if r.isJoinFunc(callExpr, c) { 83 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 84 | } 85 | } 86 | // handles binary string concatenation eg. ioutil.Readfile("/tmp/" + file + "/blob") 87 | if binExp, ok := arg.(*ast.BinaryExpr); ok { 88 | // resolve all found identities from the BinaryExpr 89 | if _, ok := gosec.FindVarIdentities(binExp, c); ok { 90 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 91 | } 92 | } 93 | 94 | if ident, ok := arg.(*ast.Ident); ok { 95 | obj := c.Info.ObjectOf(ident) 96 | if _, ok := obj.(*types.Var); ok && 97 | !gosec.TryResolve(ident, c) && 98 | !r.isFilepathClean(ident, c) { 99 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 100 | } 101 | } 102 | } 103 | } 104 | return nil, nil 105 | } 106 | 107 | // NewReadFile detects cases where we read files 108 | func NewReadFile(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 109 | rule := &readfile{ 110 | pathJoin: gosec.NewCallList(), 111 | clean: gosec.NewCallList(), 112 | CallList: gosec.NewCallList(), 113 | MetaData: gosec.MetaData{ 114 | ID: id, 115 | What: "Potential file inclusion via variable", 116 | Severity: gosec.Medium, 117 | Confidence: gosec.High, 118 | }, 119 | } 120 | rule.pathJoin.Add("path/filepath", "Join") 121 | rule.pathJoin.Add("path", "Join") 122 | rule.clean.Add("path/filepath", "Clean") 123 | rule.clean.Add("path/filepath", "Rel") 124 | rule.Add("io/ioutil", "ReadFile") 125 | rule.Add("os", "Open") 126 | rule.Add("os", "OpenFile") 127 | return rule, []ast.Node{(*ast.CallExpr)(nil)} 128 | } 129 | -------------------------------------------------------------------------------- /rules/rsa.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "fmt" 19 | "go/ast" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type weakKeyStrength struct { 25 | gosec.MetaData 26 | calls gosec.CallList 27 | bits int 28 | } 29 | 30 | func (w *weakKeyStrength) ID() string { 31 | return w.MetaData.ID 32 | } 33 | 34 | func (w *weakKeyStrength) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 35 | if callExpr := w.calls.ContainsPkgCallExpr(n, c, false); callExpr != nil { 36 | if bits, err := gosec.GetInt(callExpr.Args[1]); err == nil && bits < (int64)(w.bits) { 37 | return gosec.NewIssue(c, n, w.ID(), w.What, w.Severity, w.Confidence), nil 38 | } 39 | } 40 | return nil, nil 41 | } 42 | 43 | // NewWeakKeyStrength builds a rule that detects RSA keys < 2048 bits 44 | func NewWeakKeyStrength(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 45 | calls := gosec.NewCallList() 46 | calls.Add("crypto/rsa", "GenerateKey") 47 | bits := 2048 48 | return &weakKeyStrength{ 49 | calls: calls, 50 | bits: bits, 51 | MetaData: gosec.MetaData{ 52 | ID: id, 53 | Severity: gosec.Medium, 54 | Confidence: gosec.High, 55 | What: fmt.Sprintf("RSA keys should be at least %d bits", bits), 56 | }, 57 | }, []ast.Node{(*ast.CallExpr)(nil)} 58 | } 59 | -------------------------------------------------------------------------------- /rules/rulelist.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "sort" 19 | 20 | "github.com/cosmos/gosec/v2" 21 | "github.com/cosmos/gosec/v2/rules/sdk" 22 | ) 23 | 24 | // RuleDefinition contains the description of a rule and a mechanism to 25 | // create it. 26 | type RuleDefinition struct { 27 | ID string 28 | Description string 29 | Create gosec.RuleBuilder 30 | } 31 | 32 | // RuleList is a mapping of rule ID's to rule definitions 33 | type RuleList map[string]RuleDefinition 34 | 35 | // Builders returns all the create methods for a given rule list 36 | func (rl RuleList) Builders() map[string]gosec.RuleBuilder { 37 | ids := make([]string, 0, len(rl)) 38 | for id := range rl { 39 | ids = append(ids, id) 40 | } 41 | sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) 42 | builders := make(map[string]gosec.RuleBuilder) 43 | for _, id := range ids { 44 | def := rl[id] 45 | builders[def.ID] = def.Create 46 | } 47 | return builders 48 | } 49 | 50 | // RuleFilter can be used to include or exclude a rule depending on the return 51 | // value of the function 52 | type RuleFilter func(string) bool 53 | 54 | // NewRuleFilter is a closure that will include/exclude the rule ID's based on 55 | // the supplied boolean value. 56 | func NewRuleFilter(action bool, ruleIDs ...string) RuleFilter { 57 | rulelist := make(map[string]bool) 58 | for _, rule := range ruleIDs { 59 | rulelist[rule] = true 60 | } 61 | return func(rule string) bool { 62 | if _, found := rulelist[rule]; found { 63 | return action 64 | } 65 | return !action 66 | } 67 | } 68 | 69 | // Generate the list of rules to use 70 | func Generate(filters ...RuleFilter) RuleList { 71 | rules := []RuleDefinition{ 72 | // misc 73 | {"G101", "Look for hardcoded credentials", NewHardcodedCredentials}, 74 | {"G102", "Bind to all interfaces", NewBindsToAllNetworkInterfaces}, 75 | {"G103", "Audit the use of unsafe block", NewUsingUnsafe}, 76 | {"G104", "Audit errors not checked", NewNoErrorCheck}, 77 | {"G106", "Audit the use of ssh.InsecureIgnoreHostKey function", NewSSHHostKey}, 78 | {"G107", "Url provided to HTTP request as taint input", NewSSRFCheck}, 79 | {"G108", "Profiling endpoint is automatically exposed", NewPprofCheck}, 80 | {"G109", "Converting strconv.Atoi result to int32/int16", NewIntegerOverflowCheck}, 81 | {"G110", "Detect io.Copy instead of io.CopyN when decompression", NewDecompressionBombCheck}, 82 | 83 | // injection 84 | {"G201", "SQL query construction using format string", NewSQLStrFormat}, 85 | {"G202", "SQL query construction using string concatenation", NewSQLStrConcat}, 86 | {"G203", "Use of unescaped data in HTML templates", NewTemplateCheck}, 87 | {"G204", "Audit use of command execution", NewSubproc}, 88 | 89 | // filesystem 90 | {"G301", "Poor file permissions used when creating a directory", NewMkdirPerms}, 91 | {"G302", "Poor file permissions used when creation file or using chmod", NewFilePerms}, 92 | {"G303", "Creating tempfile using a predictable path", NewBadTempFile}, 93 | {"G304", "File path provided as taint input", NewReadFile}, 94 | {"G305", "File path traversal when extracting zip archive", NewArchive}, 95 | {"G306", "Poor file permissions used when writing to a file", NewWritePerms}, 96 | {"G307", "Unsafe defer call of a method returning an error", NewDeferredClosing}, 97 | 98 | // crypto 99 | {"G401", "Detect the usage of DES, RC4, MD5 or SHA1", NewUsesWeakCryptography}, 100 | {"G402", "Look for bad TLS connection settings", NewIntermediateTLSCheck}, 101 | {"G403", "Ensure minimum RSA key length of 2048 bits", NewWeakKeyStrength}, 102 | {"G404", "Insecure random number source (rand)", NewWeakRandCheck}, 103 | 104 | // blocklist 105 | {"G501", "Import blocklist: crypto/md5", NewBlocklistedImportMD5}, 106 | {"G502", "Import blocklist: crypto/des", NewBlocklistedImportDES}, 107 | {"G503", "Import blocklist: crypto/rc4", NewBlocklistedImportRC4}, 108 | {"G504", "Import blocklist: net/http/cgi", NewBlocklistedImportCGI}, 109 | {"G505", "Import blocklist: crypto/sha1", NewBlocklistedImportSHA1}, 110 | 111 | // memory safety 112 | {"G601", "Implicit memory aliasing in RangeStmt", NewImplicitAliasing}, 113 | 114 | // CosmosSDK Modules 115 | {"G701", "Casting integers", sdk.NewIntegerCast}, 116 | {"G702", "Import blocklist for SDK modules", sdk.NewUnsafeImport}, 117 | {"G703", "Errors that don't result in rollback", sdk.NewErrorNotPropagated}, 118 | {"G704", "Strconv invalid bitSize and cast", sdk.NewStrconvIntBitSizeOverflow}, 119 | // {"G705", "Iterating over maps undeterministically", sdk.NewMapRangingCheck}, // TODO refine this rule and make it less noisy 120 | } 121 | 122 | ruleMap := make(map[string]RuleDefinition) 123 | 124 | RULES: 125 | for _, rule := range rules { 126 | for _, filter := range filters { 127 | if filter(rule.ID) { 128 | continue RULES 129 | } 130 | } 131 | ruleMap[rule.ID] = rule 132 | } 133 | return ruleMap 134 | } 135 | -------------------------------------------------------------------------------- /rules/rules_suite_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | . "github.com/onsi/ginkgo/v2" 5 | . "github.com/onsi/gomega" 6 | 7 | "testing" 8 | ) 9 | 10 | func TestRules(t *testing.T) { 11 | RegisterFailHandler(Fail) 12 | RunSpecs(t, "Rules Suite") 13 | } 14 | -------------------------------------------------------------------------------- /rules/rules_test.go: -------------------------------------------------------------------------------- 1 | package rules_test 2 | 3 | import ( 4 | "fmt" 5 | "log" 6 | 7 | . "github.com/onsi/ginkgo/v2" 8 | . "github.com/onsi/gomega" 9 | 10 | "github.com/cosmos/gosec/v2" 11 | "github.com/cosmos/gosec/v2/rules" 12 | "github.com/cosmos/gosec/v2/testutils" 13 | ) 14 | 15 | var _ = Describe("gosec rules", func() { 16 | 17 | var ( 18 | logger *log.Logger 19 | config gosec.Config 20 | analyzer *gosec.Analyzer 21 | runner func(string, []testutils.CodeSample) 22 | buildTags []string 23 | tests bool 24 | ) 25 | 26 | BeforeEach(func() { 27 | logger, _ = testutils.NewLogger() 28 | config = gosec.NewConfig() 29 | analyzer = gosec.NewAnalyzer(config, tests, logger) 30 | runner = func(rule string, samples []testutils.CodeSample) { 31 | for n, sample := range samples { 32 | analyzer.Reset() 33 | analyzer.SetConfig(sample.Config) 34 | analyzer.LoadRules(rules.Generate(rules.NewRuleFilter(false, rule)).Builders()) 35 | pkg := testutils.NewTestPackage() 36 | defer pkg.Close() 37 | for i, code := range sample.Code { 38 | pkg.AddFile(fmt.Sprintf("sample_%d_%d.go", n, i), code) 39 | } 40 | err := pkg.Build() 41 | Expect(err).ShouldNot(HaveOccurred()) 42 | err = analyzer.Process(buildTags, pkg.Path) 43 | Expect(err).ShouldNot(HaveOccurred()) 44 | issues, _, _ := analyzer.Report() 45 | if len(issues) != sample.Errors { 46 | fmt.Println(sample.Code) 47 | } 48 | Expect(issues).Should(HaveLen(sample.Errors)) 49 | } 50 | } 51 | }) 52 | 53 | Context("report correct errors for all samples", func() { 54 | It("should detect hardcoded credentials", func() { 55 | runner("G101", testutils.SampleCodeG101) 56 | }) 57 | 58 | It("should detect binding to all network interfaces", func() { 59 | runner("G102", testutils.SampleCodeG102) 60 | }) 61 | 62 | It("should use of unsafe block", func() { 63 | runner("G103", testutils.SampleCodeG103) 64 | }) 65 | 66 | It("should detect errors not being checked", func() { 67 | runner("G104", testutils.SampleCodeG104) 68 | }) 69 | 70 | It("should detect errors not being checked in audit mode", func() { 71 | runner("G104", testutils.SampleCodeG104Audit) 72 | }) 73 | 74 | It("should detect of ssh.InsecureIgnoreHostKey function", func() { 75 | runner("G106", testutils.SampleCodeG106) 76 | }) 77 | 78 | It("should detect ssrf via http requests with variable url", func() { 79 | runner("G107", testutils.SampleCodeG107) 80 | }) 81 | 82 | It("should detect pprof endpoint", func() { 83 | runner("G108", testutils.SampleCodeG108) 84 | }) 85 | 86 | It("should detect integer overflow", func() { 87 | runner("G109", testutils.SampleCodeG109) 88 | }) 89 | 90 | It("should detect strconv bitsize mismatch", func() { 91 | runner("G704", testutils.SampleCodeStrconvBitsize) 92 | }) 93 | 94 | // It("should detect non-deterministic map ranging", func() { 95 | // runner("G705", testutils.SampleCodeMapRangingNonDeterministic) 96 | // }) 97 | 98 | It("should detect DoS vulnerability via decompression bomb", func() { 99 | runner("G110", testutils.SampleCodeG110) 100 | }) 101 | 102 | It("should detect sql injection via format strings", func() { 103 | runner("G201", testutils.SampleCodeG201) 104 | }) 105 | 106 | It("should detect sql injection via string concatenation", func() { 107 | runner("G202", testutils.SampleCodeG202) 108 | }) 109 | 110 | It("should detect unescaped html in templates", func() { 111 | runner("G203", testutils.SampleCodeG203) 112 | }) 113 | 114 | It("should detect command execution", func() { 115 | runner("G204", testutils.SampleCodeG204) 116 | }) 117 | 118 | It("should detect poor file permissions on mkdir", func() { 119 | runner("G301", testutils.SampleCodeG301) 120 | }) 121 | 122 | It("should detect poor permissions when creating or chmod a file", func() { 123 | runner("G302", testutils.SampleCodeG302) 124 | }) 125 | 126 | It("should detect insecure temp file creation", func() { 127 | runner("G303", testutils.SampleCodeG303) 128 | }) 129 | 130 | It("should detect file path provided as taint input", func() { 131 | runner("G304", testutils.SampleCodeG304) 132 | }) 133 | 134 | It("should detect file path traversal when extracting zip archive", func() { 135 | runner("G305", testutils.SampleCodeG305) 136 | }) 137 | 138 | It("should detect poor permissions when writing to a file", func() { 139 | runner("G306", testutils.SampleCodeG306) 140 | }) 141 | 142 | It("should detect unsafe defer of os.Close", func() { 143 | runner("G307", testutils.SampleCodeG307) 144 | }) 145 | 146 | It("should detect weak crypto algorithms", func() { 147 | runner("G401", testutils.SampleCodeG401) 148 | }) 149 | 150 | It("should detect weak crypto algorithms", func() { 151 | runner("G401", testutils.SampleCodeG401b) 152 | }) 153 | 154 | It("should find insecure tls settings", func() { 155 | runner("G402", testutils.SampleCodeG402) 156 | }) 157 | 158 | It("should detect weak creation of weak rsa keys", func() { 159 | runner("G403", testutils.SampleCodeG403) 160 | }) 161 | 162 | It("should find non cryptographically secure random number sources", func() { 163 | runner("G404", testutils.SampleCodeG404) 164 | }) 165 | 166 | It("should detect blocklisted imports - MD5", func() { 167 | runner("G501", testutils.SampleCodeG501) 168 | }) 169 | 170 | It("should detect blocklisted imports - DES", func() { 171 | runner("G502", testutils.SampleCodeG502) 172 | }) 173 | 174 | It("should detect blocklisted imports - RC4", func() { 175 | runner("G503", testutils.SampleCodeG503) 176 | }) 177 | 178 | It("should detect blocklisted imports - CGI (httpoxy)", func() { 179 | runner("G504", testutils.SampleCodeG504) 180 | }) 181 | 182 | It("should detect blocklisted imports - SHA1", func() { 183 | runner("G505", testutils.SampleCodeG505) 184 | }) 185 | 186 | It("should detect implicit aliasing in ForRange", func() { 187 | runner("G601", testutils.SampleCodeG601) 188 | }) 189 | 190 | }) 191 | 192 | }) 193 | -------------------------------------------------------------------------------- /rules/sdk/README.md: -------------------------------------------------------------------------------- 1 | ## cosmos-sdk rules 2 | 3 | These rules are targeted for the [Cosmos-sdk](https://github.com/cosmos/cosmos-sdk) to catch common mistakes that could be devasting. 4 | 5 | ### Table of contents 6 | - [Unsafe imports](#unsafe-imports) 7 | - [strconv unsigned integers cast to signed integers overflow](#strconv-unsigned-integers-cast-to-signed-integers-overflow) 8 | - [Non deterministic map iteration](#non-deterministic-map-iteration) 9 | 10 | ### Unsafe imports 11 | Imports like [unsafe](https://golang.org/pkg/unsafe), [runtime](https://golang.org/pkg/runtime) and [math/rand](https://golang.org/pkg/math/rand) are potential sources of non-determinism 12 | and hence they are flagged when in code. 13 | 14 | ### strconv unsigned integers cast to signed integers overflow 15 | Parsing signed integers consumes one bit less than their unsigned counterparts. The usage of [strconv.ParseUint](https://golang.org/pkg/strconv/#ParseUint) to parse a signed integer 16 | out of a string returns an unsigned 64-bit integer `uint64`. This `uint64` if cast with the wrong constant bitsize is now flagged, for example the following 17 | 18 | ```go 19 | u64, err := strconv.ParseUint(str, 10, 64) 20 | if err != nil { 21 | panic(err) 22 | } 23 | i64 := int64(u64) 24 | ``` 25 | 26 | which ideally should have been 27 | 28 | ```go 29 | u64, err := strconv.ParseUint(str, 10, 63) 30 | if err != nil { 31 | panic(err) 32 | } 33 | i64 := int64(u64) 34 | ``` 35 | 36 | ### Non deterministic map iteration 37 | In Go, iterating over maps is intentionally non-deterministic as the runtime defines. Unfortunately for us, in the Cosmos-SDK, we encountered an issue 38 | with non-deterministic upgrades in [Issue cosmos-sdk#10188](https://github.com/cosmos/cosmos-sdk/issues/10188) [PR cosmos-sdk#10189](https://github.com/cosmos/cosmos-sdk/pull/10189) that resulted from exactly this non-deterministic iteration. To ensure determinism, we only permit an iteration 39 | to retrieve the map keys and then those keys can then be sorted, so instead of 40 | ```go 41 | for k, v := range m { 42 | // Do something with key and value. 43 | _, _ = k, v 44 | } 45 | ``` 46 | 47 | the requested pattern is instead 48 | ```go 49 | keys := make([]string, 0, len(m)) 50 | for k := range m { 51 | keys = append(keys, k) 52 | } 53 | sort.Strings(keys) 54 | 55 | for _, key := range keys { 56 | // Use the value. 57 | _ = m[key] 58 | } 59 | ``` 60 | -------------------------------------------------------------------------------- /rules/sdk/blocklist.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sdk 16 | 17 | import ( 18 | "go/ast" 19 | "path/filepath" 20 | "strings" 21 | 22 | "github.com/cosmos/gosec/v2" 23 | ) 24 | 25 | type blocklistedImport struct { 26 | gosec.MetaData 27 | Blocklisted map[string]string 28 | } 29 | 30 | func unquote(original string) string { 31 | copy := strings.TrimSpace(original) 32 | copy = strings.TrimLeft(copy, `"`) 33 | return strings.TrimRight(copy, `"`) 34 | } 35 | 36 | func (r *blocklistedImport) ID() string { 37 | return r.MetaData.ID 38 | } 39 | 40 | // forbiddenFromBlockedImports returns true if the package isn't allowed to import blocklisted/unsafe 41 | // packages; there are some packages though that we should allow unsafe imports given that they 42 | // critically need randomness for example cryptographic code, testing and simulation packages. 43 | // Please see https://github.com/cosmos/gosec/issues/44. 44 | func forbiddenFromBlockedImports(ctx *gosec.Context) bool { 45 | switch pkg := ctx.Pkg.Name(); pkg { 46 | case "codegen", "crypto", "depinject", "secp256k1", "simapp", "simulation", "testutil": 47 | // These packages rely on imports of "unsafe", "crypto/rand", "math/rand" 48 | // for their core functionality like randomization e.g. in simulation or get 49 | // data for randomizing data. 50 | return false 51 | default: 52 | pkgPath, err := gosec.GetPkgAbsPath(pkg) 53 | if err != nil { 54 | return true 55 | } 56 | 57 | splits := strings.Split(pkgPath, string(filepath.Separator)) 58 | for _, split := range splits { 59 | if split == "crypto" { 60 | return false 61 | } 62 | } 63 | // Everything else is forbidden from unsafe imports. 64 | return true 65 | } 66 | } 67 | 68 | func (r *blocklistedImport) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 69 | if node, ok := n.(*ast.ImportSpec); ok && forbiddenFromBlockedImports(c) { 70 | if description, ok := r.Blocklisted[unquote(node.Path.Value)]; ok { 71 | return gosec.NewIssue(c, node, r.ID(), description, r.Severity, r.Confidence), nil 72 | } 73 | } 74 | return nil, nil 75 | } 76 | 77 | // NewBlocklistedImports reports when a blocklisted import is being used. 78 | // Typically when a deprecated technology is being used. 79 | func NewBlocklistedImports(id string, conf gosec.Config, blocklist map[string]string) (gosec.Rule, []ast.Node) { 80 | return &blocklistedImport{ 81 | MetaData: gosec.MetaData{ 82 | ID: id, 83 | Severity: gosec.Medium, 84 | Confidence: gosec.High, 85 | }, 86 | Blocklisted: blocklist, 87 | }, []ast.Node{(*ast.ImportSpec)(nil)} 88 | } 89 | 90 | // NewUnsafeImport fails if any of "unsafe", "reflect", "crypto/rand", "math/rand" are imported. 91 | func NewUnsafeImport(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 92 | return NewBlocklistedImports(id, conf, map[string]string{ 93 | // unsafe exposes memory bugs 94 | "unsafe": "Blocklisted import unsafe", 95 | 96 | // reflect allows reading private fields and calling private 97 | // methods from other pkgs. 98 | "reflect": "Blocklisted import reflect", 99 | 100 | // runtime data can be parsed to get pointer values. 101 | // but without unsafe, does it matter? 102 | "runtime": "Blocklisted import runtime", 103 | 104 | // rand is non-deterministic. 105 | // TODO: module.RandomizedParams takes a math/rand.Rand 106 | "math/rand": "Blocklisted import math/rand", 107 | "crypto/rand": "Blocklisted import crypto/rand", 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /rules/sdk/errors.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sdk 16 | 17 | import ( 18 | "go/ast" 19 | "go/types" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type noErrorCheck struct { 25 | gosec.MetaData 26 | } 27 | 28 | func (r *noErrorCheck) ID() string { 29 | return r.MetaData.ID 30 | } 31 | 32 | func returnsError(callExpr *ast.CallExpr, ctx *gosec.Context) int { 33 | if tv := ctx.Info.TypeOf(callExpr); tv != nil { 34 | switch t := tv.(type) { 35 | case *types.Tuple: 36 | for pos := 0; pos < t.Len(); pos++ { 37 | variable := t.At(pos) 38 | if variable != nil && variable.Type().String() == "error" { 39 | return pos 40 | } 41 | } 42 | case *types.Named: 43 | if t.String() == "error" { 44 | return 0 45 | } 46 | } 47 | } 48 | return -1 49 | } 50 | 51 | func (r *noErrorCheck) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 52 | // TODO: when an error is returned, ensure it's followed by a check that `err != nil`, 53 | // and that the BlockStmt there returns the error 54 | 55 | switch stmt := n.(type) { 56 | case *ast.AssignStmt: 57 | for _, expr := range stmt.Rhs { 58 | callExpr, ok := expr.(*ast.CallExpr) 59 | if !ok { 60 | continue 61 | } 62 | 63 | if allowedToNotReturnErr.ContainsCallExpr(callExpr, ctx) != nil { 64 | continue 65 | } 66 | 67 | pos := returnsError(callExpr, ctx) 68 | if pos < 0 || pos >= len(stmt.Lhs) { 69 | return nil, nil 70 | } 71 | id, ok := stmt.Lhs[pos].(*ast.Ident) 72 | if !ok { 73 | // don't think this should ever happen 74 | return gosec.NewIssue(ctx, n, r.ID(), "PANIC!", r.Severity, r.Confidence), nil 75 | } else if ok && id.Name == "_" { 76 | // error is just ignored! 77 | return gosec.NewIssue(ctx, n, r.ID(), r.What, r.Severity, r.Confidence), nil 78 | } 79 | 80 | // TODO: next line should check `id.Name != nil`, 81 | // and the BlockStmt that follows should have a ReturnStmt 82 | // that includes the id.Name 83 | } 84 | } 85 | return nil, nil 86 | } 87 | 88 | var allowedToNotReturnErr gosec.CallList 89 | 90 | func init() { 91 | allowedToNotReturnErr = gosec.NewCallList() 92 | allowedToNotReturnErr.AddAll("bytes.Buffer", "Write", "WriteByte", "WriteRune", "WriteString") 93 | allowedToNotReturnErr.AddAll("fmt", "Print", "Printf", "Println", "Fprint", "Fprintf", "Fprintln") 94 | allowedToNotReturnErr.AddAll("strings.Builder", "Write", "WriteByte", "WriteRune", "WriteString") 95 | allowedToNotReturnErr.Add("io.PipeWriter", "CloseWithError") 96 | allowedToNotReturnErr.Add("hash.Hash", "Write") 97 | allowedToNotReturnErr.AddAll("github.com/spf13/pflag.FlagSet", "GetBool", "GetString", "GetUint32", "GetBool", "GetInt64", "GetUint64") 98 | } 99 | 100 | // NewErrorNotPropagated detects if a returned error is not propagated up the stack. 101 | func NewErrorNotPropagated(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 102 | return &noErrorCheck{ 103 | MetaData: gosec.MetaData{ 104 | ID: id, 105 | Severity: gosec.Low, 106 | Confidence: gosec.High, 107 | What: "Returned error is not propagated up the stack.", 108 | }, 109 | }, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)} 110 | } 111 | -------------------------------------------------------------------------------- /rules/sdk/int_end_conversion_test.go: -------------------------------------------------------------------------------- 1 | package sdk 2 | 3 | import "testing" 4 | 5 | func TestCanOverflowChecks32Bits(t *testing.T) { 6 | if !is32Bit { 7 | t.Skip("Not running on a 64-bit machine!") 8 | } 9 | 10 | cases := []struct { 11 | endKind string 12 | wantOverflow bool 13 | }{ 14 | {"int8", true}, 15 | {"int16", true}, 16 | {"int32", false}, 17 | {"int64", false}, 18 | {"int", false}, 19 | {"uint8", true}, 20 | {"uint16", true}, 21 | {"uint32", false}, 22 | {"uint64", false}, 23 | {"uint", false}, 24 | } 25 | 26 | for _, tt := range cases { 27 | tt := tt 28 | t.Run(tt.endKind, func(t *testing.T) { 29 | if got := canLenOverflow32(tt.endKind); got != tt.wantOverflow { 30 | t.Fatalf("Mismatch\n\tGot: %t\n\tWant:%t", got, tt.wantOverflow) 31 | } 32 | }) 33 | } 34 | } 35 | 36 | func TestCanOverflowChecks64Bits(t *testing.T) { 37 | if is32Bit { 38 | t.Skip("Not running on a 32-bit machine!") 39 | } 40 | 41 | cases := []struct { 42 | endKind string 43 | wantOverflow bool 44 | }{ 45 | {"int8", true}, 46 | {"int16", true}, 47 | {"int32", true}, 48 | {"int", false}, 49 | {"int64", false}, 50 | {"uint8", true}, 51 | {"uint16", true}, 52 | {"uint32", true}, 53 | {"uint64", false}, 54 | {"uint", false}, 55 | } 56 | 57 | for _, tt := range cases { 58 | tt := tt 59 | t.Run(tt.endKind, func(t *testing.T) { 60 | if got := canLenOverflow64(tt.endKind); got != tt.wantOverflow { 61 | t.Fatalf("Mismatch\n\tGot: %t\n\tWant:%t", got, tt.wantOverflow) 62 | } 63 | }) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /rules/sdk/integer.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sdk 16 | 17 | import ( 18 | "go/ast" 19 | "go/types" 20 | "strconv" 21 | "strings" 22 | 23 | "github.com/cosmos/gosec/v2" 24 | ) 25 | 26 | // originally copied and simplified from the rules/integer_overflow.go 27 | type integerOverflowCheck struct { 28 | gosec.MetaData 29 | } 30 | 31 | func (i *integerOverflowCheck) ID() string { 32 | return i.MetaData.ID 33 | } 34 | 35 | func hasAnyPrefix(src string, prefixes ...string) bool { 36 | for _, prefix := range prefixes { 37 | if strings.HasPrefix(src, prefix) { 38 | return true 39 | } 40 | } 41 | return false 42 | } 43 | 44 | // To catch integer type conversion, check if we ever 45 | // call functions `uintX(y)` or `intX(y)` for any X and y, 46 | // where y is not an int literal. 47 | // TODO: restrict it to just the possible bit-sizes for X (unspecified, 8, 16, 32, 64) 48 | // TODO: check if y's bit-size is greater than X 49 | func (i *integerOverflowCheck) Match(node ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 50 | // ignore if it's protobuf 51 | fileName := ctx.FileSet.File(node.Pos()).Name() 52 | if strings.HasSuffix(fileName, ".pb.go") { 53 | return nil, nil 54 | } 55 | 56 | switch n := node.(type) { 57 | case *ast.CallExpr: 58 | fun, ok := n.Fun.(*ast.Ident) 59 | if !ok { 60 | return nil, nil 61 | } 62 | 63 | if len(n.Args) == 0 { 64 | return nil, nil 65 | } 66 | 67 | arg := n.Args[0] 68 | argT := ctx.Info.TypeOf(arg) 69 | if argT == nil { 70 | // TODO: Perhaps log and investigate this case more. 71 | return nil, nil 72 | } 73 | fnType := ctx.Info.TypeOf(fun) 74 | if fnType == nil { 75 | // TODO: Perhaps log and investigate this case more. 76 | return nil, nil 77 | } 78 | 79 | argType := argT.Underlying() 80 | destType := fnType.Underlying() 81 | intCast := hasAnyPrefix(destType.String(), "int", "uint") 82 | if !intCast { 83 | return nil, nil 84 | } 85 | 86 | // Detect intX(y) and uintX(y) for any X, where y is not an int literal. 87 | // n.Args[0] is of type ast.Expr. It's the arg to the type conversion. 88 | // If the expression string is a constant integer, then ignore. 89 | // TODO: check that the constant will actually fit and wont overflow? 90 | exprString := types.ExprString(arg) 91 | intLiteral, err := strconv.Atoi(exprString) 92 | if err == nil { 93 | // TODO: probably use ParseInt and check if it fits in the target. 94 | _ = intLiteral 95 | return nil, nil 96 | } 97 | 98 | switch arg := arg.(type) { 99 | case *ast.CallExpr: 100 | // len() returns an int that is always >= 0, so it will fit in a uint, uint64, or int64. 101 | argFun, ok := arg.Fun.(*ast.Ident) 102 | if !ok || argFun.Name != "len" { 103 | break 104 | } 105 | 106 | // Please see the rules for determining if *int*(len(...)) can overflow 107 | // as per: https://github.com/cosmos/gosec/issues/54 108 | lenCanOverflow := canLenOverflow64 109 | if is32Bit { 110 | lenCanOverflow = canLenOverflow32 111 | } 112 | 113 | if lenCanOverflow(fun.Name) { 114 | return gosec.NewIssue(ctx, n, i.ID(), i.What, i.Severity, i.Confidence), nil 115 | } 116 | return nil, nil 117 | } 118 | 119 | // If the argument is being cast to its underlying type, there's no risk. 120 | if argType == destType { 121 | return nil, nil 122 | } 123 | 124 | // Check if both are uint* values. 125 | argIsUint := hasAnyPrefix(argType.String(), "uint") 126 | if argIsUint && !canBothUintsOverflow(argType.String(), destType.String()) { 127 | return nil, nil 128 | } 129 | 130 | // Check if both are int* values. 131 | argIsInt := hasAnyPrefix(argType.String(), "int") 132 | if argIsInt && !canBothIntToIntOverflow(argType.String(), destType.String()) { 133 | return nil, nil 134 | } 135 | 136 | // ALl other cases should be flagged. 137 | return gosec.NewIssue(ctx, n, i.ID(), i.What, i.Severity, i.Confidence), nil 138 | } 139 | 140 | return nil, nil 141 | } 142 | 143 | // NewIntegerCast detects if there is potential Integer OverFlow 144 | func NewIntegerCast(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 145 | return &integerOverflowCheck{ 146 | MetaData: gosec.MetaData{ 147 | ID: id, 148 | Severity: gosec.High, 149 | Confidence: gosec.Medium, 150 | What: "Potential integer overflow by integer type conversion", 151 | }, 152 | }, []ast.Node{(*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)} 153 | } 154 | 155 | // Please see the rules at https://github.com/cosmos/gosec/issues/54 156 | func canLenOverflow64(destKind string) bool { 157 | switch destKind { 158 | case "int8", "uint8", "int16", "uint16": 159 | return true 160 | 161 | case "uint64": 162 | // uint64([0, maxInt64]) 163 | return false 164 | 165 | case "uint32": 166 | // uint32([0, maxInt64]) 167 | return true 168 | 169 | case "uint": 170 | // uint => uint64 => uint64([0, maxInt64]) 171 | return false 172 | 173 | case "int64": 174 | // int64([0, maxInt64]) 175 | return false 176 | 177 | case "int32": 178 | // int32([0, maxInt64]) 179 | return true 180 | 181 | case "int": 182 | // int64([0, maxInt64]) 183 | return false 184 | 185 | default: 186 | return true 187 | } 188 | } 189 | 190 | const s = 1 191 | const is32Bit = (^uint(s-1))>>32 == 0 // #nosec 192 | 193 | // Please see the rules at https://github.com/cosmos/gosec/issues/54 194 | func canLenOverflow32(destKind string) bool { 195 | switch destKind { 196 | case "int8", "uint8", "int16", "uint16": 197 | return true 198 | 199 | case "uint64": 200 | // uint64([0, maxInt32]) 201 | return false 202 | 203 | case "uint32": 204 | // uint32([0, maxInt32]) 205 | return false 206 | 207 | case "uint": 208 | // uint => uint32 => uint32([0, maxInt32]) 209 | return false 210 | 211 | case "int64": 212 | // int64([0, maxInt32]) 213 | return false 214 | 215 | case "int32": 216 | // int32([0, maxInt32]) 217 | return false 218 | 219 | case "int": 220 | // int => int32 => int32([0, maxInt32]) 221 | return false 222 | 223 | default: 224 | return true 225 | } 226 | } 227 | 228 | func canBothUintsOverflow(srcKind, destKind string) bool { 229 | bothUints := hasAnyPrefix(srcKind, "uint") && hasAnyPrefix(destKind, "uint") 230 | if !bothUints { 231 | return true 232 | } 233 | 234 | if destKind == "uint" { 235 | // Only in 32-bit is uint equal to uint32 hence can it overflow if src is uint64. 236 | return srcKind == "uint64" && is32Bit 237 | } 238 | if destKind == "uint64" { 239 | // Casting any uint type to uint64 cannot overflow. 240 | return false 241 | } 242 | if destKind == "uint32" { 243 | // Only uint64 or uint (when in 64-bits) can overflow when being cast to uint32. 244 | return srcKind == "uint64" || (srcKind == "uint" && !is32Bit) 245 | } 246 | if destKind == "uint16" { 247 | // Everything except "uint8" and "uint16" can overflow when cast to uint16. 248 | return srcKind == "uint64" || srcKind == "uint32" || srcKind == "uint" 249 | } 250 | if destKind == "uint8" { 251 | // Everything that isn't "uint8" will overflow when cast to uint8. 252 | return srcKind != "uint8" 253 | } 254 | return true 255 | } 256 | 257 | func canBothIntToIntOverflow(srcKind, destKind string) bool { 258 | bothInts := hasAnyPrefix(srcKind, "int") && hasAnyPrefix(destKind, "int") 259 | if !bothInts { 260 | return true 261 | } 262 | 263 | if destKind == "int" { 264 | // Only in 32-bit is int equal to int32 hence can it overflow if src is int64. 265 | return srcKind == "int64" && is32Bit 266 | } 267 | if destKind == "int64" { 268 | // Casting any int type to int64 cannot overflow. 269 | return false 270 | } 271 | if destKind == "int32" { 272 | // Only int64 or int (when in 64-bits) can overflow when being cast to int32. 273 | return srcKind == "int64" || (srcKind == "int" && !is32Bit) 274 | } 275 | if destKind == "int16" { 276 | // Everything except "int8" and "int16" can overflow when cast to int16. 277 | return srcKind == "int64" || srcKind == "int32" || srcKind == "int" 278 | } 279 | if destKind == "int8" { 280 | // Everything that isn't "int8" will overflow when cast to int8. 281 | return srcKind != "int8" 282 | } 283 | return true 284 | } 285 | -------------------------------------------------------------------------------- /rules/sdk/strconv_bitsize_mismatch.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2021 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package sdk 16 | 17 | import ( 18 | "fmt" 19 | "go/ast" 20 | "strconv" 21 | 22 | "github.com/cosmos/gosec/v2" 23 | ) 24 | 25 | type bitsizeOverflowCheck struct { 26 | gosec.MetaData 27 | calls gosec.CallList 28 | } 29 | 30 | func (bc *bitsizeOverflowCheck) ID() string { 31 | return bc.MetaData.ID 32 | } 33 | 34 | func (bc *bitsizeOverflowCheck) Match(node ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 35 | var parseUintVarObj map[*ast.Object]ast.Node 36 | 37 | // Given that the code could be splayed over multiple line, we 38 | // examine ctx.PassedValues to check for temporarily stored data. 39 | if retr, ok := ctx.PassedValues[bc.ID()]; !ok { 40 | parseUintVarObj = make(map[*ast.Object]ast.Node) 41 | ctx.PassedValues[bc.ID()] = parseUintVarObj 42 | } else if saved, ok := retr.(map[*ast.Object]ast.Node); ok { 43 | parseUintVarObj = saved 44 | } else { 45 | return nil, fmt.Errorf("ctx.PassedValues[%s] is of type %T, want %T", bc.ID(), retr, parseUintVarObj) 46 | } 47 | 48 | // strconv.ParseUint* 49 | // To reduce false positives, detect code that is converted to any of: int16, int32, int64 only. 50 | switch n := node.(type) { 51 | case *ast.AssignStmt: 52 | for _, expr := range n.Rhs { 53 | callExpr, ok := expr.(*ast.CallExpr) 54 | if !ok { 55 | continue 56 | } 57 | 58 | if bc.calls.ContainsPkgCallExpr(callExpr, ctx, false) == nil { 59 | continue 60 | } 61 | 62 | ident, ok := n.Lhs[0].(*ast.Ident) 63 | if ok && ident.Name != "_" { 64 | parseUintVarObj[ident.Obj] = n 65 | } 66 | } 67 | 68 | case *ast.CallExpr: 69 | fn, ok := n.Fun.(*ast.Ident) 70 | if !ok { 71 | return nil, nil 72 | } 73 | 74 | switch fn.Name { 75 | default: 76 | return nil, nil 77 | 78 | case "int", "int16", "int32", "int64": 79 | ident, ok := n.Args[0].(*ast.Ident) 80 | if !ok { 81 | return nil, nil 82 | } 83 | 84 | nFound, ok := parseUintVarObj[ident.Obj] 85 | if !ok { 86 | return nil, nil 87 | } 88 | 89 | stmt, ok := nFound.(*ast.AssignStmt) 90 | if !ok { 91 | return nil, nil 92 | } 93 | r0 := stmt.Rhs[0] 94 | call, ok := r0.(*ast.CallExpr) 95 | if !ok { 96 | return nil, nil 97 | } 98 | bitSizeLit, ok := call.Args[2].(*ast.BasicLit) 99 | if !ok { 100 | return nil, nil 101 | } 102 | 103 | // Actually strconv parse it. 104 | bitSize, err := strconv.Atoi(bitSizeLit.Value) 105 | if err != nil { 106 | failure := fmt.Sprintf("Invalid bitSize %q parse failure: %v", bitSizeLit.Value, err) 107 | return gosec.NewIssue(ctx, nFound, bc.ID(), failure, bc.Severity, bc.Confidence), nil 108 | } 109 | 110 | failed := false 111 | switch { 112 | case fn.Name == "int16" && bitSize >= 16: 113 | failed = true 114 | case fn.Name == "int64" && bitSize >= 64: 115 | failed = true 116 | case fn.Name == "int32" && bitSize >= 32: 117 | failed = true 118 | case fn.Name == "int" && (bitSize == 32 || bitSize >= 64): 119 | failed = true 120 | } 121 | 122 | if !failed { 123 | return nil, nil 124 | } 125 | 126 | // Otherwise compose the message now. 127 | failure := fmt.Sprintf("Overflow in bitSize of %d for %q", bitSize, fn.Name) 128 | 129 | // The value was found, next let's check for the size of: 130 | // strconv.ParseUint(str, base, digits) 131 | // Awesome, we found the conversion to int* 132 | // Next we need to examine what the bitSize was. 133 | return gosec.NewIssue(ctx, nFound, bc.ID(), failure, bc.Severity, bc.Confidence), nil 134 | } 135 | } 136 | 137 | return nil, nil 138 | } 139 | 140 | // NewStrconvIntBitSizeOverflow returns an error if a constant bitSize is used 141 | // for a cast signed value that was retrieved from strconv.ParseUint. 142 | func NewStrconvIntBitSizeOverflow(id string, config gosec.Config) (rule gosec.Rule, nodes []ast.Node) { 143 | calls := gosec.NewCallList() 144 | calls.Add("strconv", "ParseUint") 145 | 146 | bc := &bitsizeOverflowCheck{ 147 | MetaData: gosec.MetaData{ 148 | ID: id, 149 | Severity: gosec.High, 150 | Confidence: gosec.Medium, 151 | What: "Overflow due to wrong bitsize in strconv.ParseUint yet cast from uint64 to int*", 152 | }, 153 | calls: calls, 154 | } 155 | 156 | nodes = append(nodes, (*ast.FuncDecl)(nil), (*ast.AssignStmt)(nil), (*ast.CallExpr)(nil)) 157 | return bc, nodes 158 | } 159 | -------------------------------------------------------------------------------- /rules/sql.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "regexp" 20 | "strings" 21 | 22 | "github.com/cosmos/gosec/v2" 23 | ) 24 | 25 | type sqlStatement struct { 26 | gosec.MetaData 27 | gosec.CallList 28 | 29 | // Contains a list of patterns which must all match for the rule to match. 30 | patterns []*regexp.Regexp 31 | } 32 | 33 | func (s *sqlStatement) ID() string { 34 | return s.MetaData.ID 35 | } 36 | 37 | // See if the string matches the patterns for the statement. 38 | func (s *sqlStatement) MatchPatterns(str string) bool { 39 | for _, pattern := range s.patterns { 40 | if !pattern.MatchString(str) { 41 | return false 42 | } 43 | } 44 | return true 45 | } 46 | 47 | type sqlStrConcat struct { 48 | sqlStatement 49 | } 50 | 51 | func (s *sqlStrConcat) ID() string { 52 | return s.MetaData.ID 53 | } 54 | 55 | // see if we can figure out what it is 56 | func (s *sqlStrConcat) checkObject(n *ast.Ident, c *gosec.Context) bool { 57 | if n.Obj != nil { 58 | return n.Obj.Kind != ast.Var && n.Obj.Kind != ast.Fun 59 | } 60 | 61 | // Try to resolve unresolved identifiers using other files in same package 62 | for _, file := range c.PkgFiles { 63 | if node, ok := file.Scope.Objects[n.String()]; ok { 64 | return node.Kind != ast.Var && node.Kind != ast.Fun 65 | } 66 | } 67 | return false 68 | } 69 | 70 | // checkQuery verifies if the query parameters is a string concatenation 71 | func (s *sqlStrConcat) checkQuery(call *ast.CallExpr, ctx *gosec.Context) (*gosec.Issue, error) { 72 | _, fnName, err := gosec.GetCallInfo(call, ctx) 73 | if err != nil { 74 | return nil, err 75 | } 76 | var query ast.Node 77 | if strings.HasSuffix(fnName, "Context") { 78 | query = call.Args[1] 79 | } else { 80 | query = call.Args[0] 81 | } 82 | 83 | if be, ok := query.(*ast.BinaryExpr); ok { 84 | operands := gosec.GetBinaryExprOperands(be) 85 | if start, ok := operands[0].(*ast.BasicLit); ok { 86 | if str, e := gosec.GetString(start); e == nil { 87 | if !s.MatchPatterns(str) { 88 | return nil, nil 89 | } 90 | } 91 | for _, op := range operands[1:] { 92 | if _, ok := op.(*ast.BasicLit); ok { 93 | continue 94 | } 95 | if op, ok := op.(*ast.Ident); ok && s.checkObject(op, ctx) { 96 | continue 97 | } 98 | return gosec.NewIssue(ctx, be, s.ID(), s.What, s.Severity, s.Confidence), nil 99 | } 100 | } 101 | } 102 | 103 | return nil, nil 104 | } 105 | 106 | // Checks SQL query concatenation issues such as "SELECT * FROM table WHERE " + " ' OR 1=1" 107 | func (s *sqlStrConcat) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 108 | switch stmt := n.(type) { 109 | case *ast.AssignStmt: 110 | for _, expr := range stmt.Rhs { 111 | if sqlQueryCall, ok := expr.(*ast.CallExpr); ok && s.ContainsCallExpr(expr, ctx) != nil { 112 | return s.checkQuery(sqlQueryCall, ctx) 113 | } 114 | } 115 | case *ast.ExprStmt: 116 | if sqlQueryCall, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(stmt.X, ctx) != nil { 117 | return s.checkQuery(sqlQueryCall, ctx) 118 | } 119 | } 120 | return nil, nil 121 | } 122 | 123 | // NewSQLStrConcat looks for cases where we are building SQL strings via concatenation 124 | func NewSQLStrConcat(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 125 | rule := &sqlStrConcat{ 126 | sqlStatement: sqlStatement{ 127 | patterns: []*regexp.Regexp{ 128 | regexp.MustCompile(`(?i)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) `), 129 | }, 130 | MetaData: gosec.MetaData{ 131 | ID: id, 132 | Severity: gosec.Medium, 133 | Confidence: gosec.High, 134 | What: "SQL string concatenation", 135 | }, 136 | CallList: gosec.NewCallList(), 137 | }, 138 | } 139 | 140 | rule.AddAll("*database/sql.DB", "Query", "QueryContext", "QueryRow", "QueryRowContext") 141 | rule.AddAll("*database/sql.Tx", "Query", "QueryContext", "QueryRow", "QueryRowContext") 142 | return rule, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)} 143 | } 144 | 145 | type sqlStrFormat struct { 146 | gosec.CallList 147 | sqlStatement 148 | fmtCalls gosec.CallList 149 | noIssue gosec.CallList 150 | noIssueQuoted gosec.CallList 151 | } 152 | 153 | // see if we can figure out what it is 154 | func (s *sqlStrFormat) constObject(e ast.Expr, c *gosec.Context) bool { 155 | n, ok := e.(*ast.Ident) 156 | if !ok { 157 | return false 158 | } 159 | 160 | if n.Obj != nil { 161 | return n.Obj.Kind == ast.Con 162 | } 163 | 164 | // Try to resolve unresolved identifiers using other files in same package 165 | for _, file := range c.PkgFiles { 166 | if node, ok := file.Scope.Objects[n.String()]; ok { 167 | return node.Kind == ast.Con 168 | } 169 | } 170 | return false 171 | } 172 | 173 | func (s *sqlStrFormat) checkQuery(call *ast.CallExpr, ctx *gosec.Context) (*gosec.Issue, error) { 174 | _, fnName, err := gosec.GetCallInfo(call, ctx) 175 | if err != nil { 176 | return nil, err 177 | } 178 | var query ast.Node 179 | if strings.HasSuffix(fnName, "Context") { 180 | query = call.Args[1] 181 | } else { 182 | query = call.Args[0] 183 | } 184 | 185 | if ident, ok := query.(*ast.Ident); ok && ident.Obj != nil { 186 | decl := ident.Obj.Decl 187 | if assign, ok := decl.(*ast.AssignStmt); ok { 188 | for _, expr := range assign.Rhs { 189 | issue, err := s.checkFormatting(expr, ctx) 190 | if issue != nil { 191 | return issue, err 192 | } 193 | } 194 | } 195 | } 196 | 197 | return nil, nil 198 | } 199 | 200 | func (s *sqlStrFormat) checkFormatting(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 201 | // argIndex changes the function argument which gets matched to the regex 202 | argIndex := 0 203 | if node := s.fmtCalls.ContainsPkgCallExpr(n, ctx, false); node != nil { 204 | // if the function is fmt.Fprintf, search for SQL statement in Args[1] instead 205 | if sel, ok := node.Fun.(*ast.SelectorExpr); ok { 206 | if sel.Sel.Name == "Fprintf" { 207 | // if os.Stderr or os.Stdout is in Arg[0], mark as no issue 208 | if arg, ok := node.Args[0].(*ast.SelectorExpr); ok { 209 | if ident, ok := arg.X.(*ast.Ident); ok { 210 | if s.noIssue.Contains(ident.Name, arg.Sel.Name) { 211 | return nil, nil 212 | } 213 | } 214 | } 215 | // the function is Fprintf so set argIndex = 1 216 | argIndex = 1 217 | } 218 | } 219 | 220 | // no formatter 221 | if len(node.Args) == 0 { 222 | return nil, nil 223 | } 224 | 225 | var formatter string 226 | 227 | // concats callexpr arg strings together if needed before regex evaluation 228 | if argExpr, ok := node.Args[argIndex].(*ast.BinaryExpr); ok { 229 | if fullStr, ok := gosec.ConcatString(argExpr); ok { 230 | formatter = fullStr 231 | } 232 | } else if arg, e := gosec.GetString(node.Args[argIndex]); e == nil { 233 | formatter = arg 234 | } 235 | if len(formatter) <= 0 { 236 | return nil, nil 237 | } 238 | 239 | // If all formatter args are quoted or constant, then the SQL construction is safe 240 | if argIndex+1 < len(node.Args) { 241 | allSafe := true 242 | for _, arg := range node.Args[argIndex+1:] { 243 | if n := s.noIssueQuoted.ContainsPkgCallExpr(arg, ctx, true); n == nil && !s.constObject(arg, ctx) { 244 | allSafe = false 245 | break 246 | } 247 | } 248 | if allSafe { 249 | return nil, nil 250 | } 251 | } 252 | if s.MatchPatterns(formatter) { 253 | return gosec.NewIssue(ctx, n, s.ID(), s.What, s.Severity, s.Confidence), nil 254 | } 255 | } 256 | return nil, nil 257 | } 258 | 259 | // Check SQL query formatting issues such as "fmt.Sprintf("SELECT * FROM foo where '%s', userInput)" 260 | func (s *sqlStrFormat) Match(n ast.Node, ctx *gosec.Context) (*gosec.Issue, error) { 261 | switch stmt := n.(type) { 262 | case *ast.AssignStmt: 263 | for _, expr := range stmt.Rhs { 264 | if sqlQueryCall, ok := expr.(*ast.CallExpr); ok && s.ContainsCallExpr(expr, ctx) != nil { 265 | return s.checkQuery(sqlQueryCall, ctx) 266 | } 267 | } 268 | case *ast.ExprStmt: 269 | if sqlQueryCall, ok := stmt.X.(*ast.CallExpr); ok && s.ContainsCallExpr(stmt.X, ctx) != nil { 270 | return s.checkQuery(sqlQueryCall, ctx) 271 | } 272 | } 273 | return nil, nil 274 | } 275 | 276 | // NewSQLStrFormat looks for cases where we're building SQL query strings using format strings 277 | func NewSQLStrFormat(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 278 | rule := &sqlStrFormat{ 279 | CallList: gosec.NewCallList(), 280 | fmtCalls: gosec.NewCallList(), 281 | noIssue: gosec.NewCallList(), 282 | noIssueQuoted: gosec.NewCallList(), 283 | sqlStatement: sqlStatement{ 284 | patterns: []*regexp.Regexp{ 285 | regexp.MustCompile("(?i)(SELECT|DELETE|INSERT|UPDATE|INTO|FROM|WHERE) "), 286 | regexp.MustCompile("%[^bdoxXfFp]"), 287 | }, 288 | MetaData: gosec.MetaData{ 289 | ID: id, 290 | Severity: gosec.Medium, 291 | Confidence: gosec.High, 292 | What: "SQL string formatting", 293 | }, 294 | }, 295 | } 296 | rule.AddAll("*database/sql.DB", "Query", "QueryContext", "QueryRow", "QueryRowContext") 297 | rule.AddAll("*database/sql.Tx", "Query", "QueryContext", "QueryRow", "QueryRowContext") 298 | rule.fmtCalls.AddAll("fmt", "Sprint", "Sprintf", "Sprintln", "Fprintf") 299 | rule.noIssue.AddAll("os", "Stdout", "Stderr") 300 | rule.noIssueQuoted.Add("github.com/lib/pq", "QuoteIdentifier") 301 | 302 | return rule, []ast.Node{(*ast.AssignStmt)(nil), (*ast.ExprStmt)(nil)} 303 | } 304 | -------------------------------------------------------------------------------- /rules/ssh.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | ) 8 | 9 | type sshHostKey struct { 10 | gosec.MetaData 11 | pkg string 12 | calls []string 13 | } 14 | 15 | func (r *sshHostKey) ID() string { 16 | return r.MetaData.ID 17 | } 18 | 19 | func (r *sshHostKey) Match(n ast.Node, c *gosec.Context) (gi *gosec.Issue, err error) { 20 | if _, matches := gosec.MatchCallByPackage(n, c, r.pkg, r.calls...); matches { 21 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 22 | } 23 | return nil, nil 24 | } 25 | 26 | // NewSSHHostKey rule detects the use of insecure ssh HostKeyCallback. 27 | func NewSSHHostKey(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 28 | return &sshHostKey{ 29 | pkg: "golang.org/x/crypto/ssh", 30 | calls: []string{"InsecureIgnoreHostKey"}, 31 | MetaData: gosec.MetaData{ 32 | ID: id, 33 | What: "Use of ssh InsecureIgnoreHostKey should be audited", 34 | Severity: gosec.Medium, 35 | Confidence: gosec.High, 36 | }, 37 | }, []ast.Node{(*ast.CallExpr)(nil)} 38 | } 39 | -------------------------------------------------------------------------------- /rules/ssrf.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "github.com/cosmos/gosec/v2" 8 | ) 9 | 10 | type ssrf struct { 11 | gosec.MetaData 12 | gosec.CallList 13 | } 14 | 15 | // ID returns the identifier for this rule 16 | func (r *ssrf) ID() string { 17 | return r.MetaData.ID 18 | } 19 | 20 | // ResolveVar tries to resolve the first argument of a call expression 21 | // The first argument is the url 22 | func (r *ssrf) ResolveVar(n *ast.CallExpr, c *gosec.Context) bool { 23 | if len(n.Args) > 0 { 24 | arg := n.Args[0] 25 | if ident, ok := arg.(*ast.Ident); ok { 26 | obj := c.Info.ObjectOf(ident) 27 | if _, ok := obj.(*types.Var); ok { 28 | scope := c.Pkg.Scope() 29 | if scope != nil && scope.Lookup(ident.Name) != nil { 30 | // a URL defined in a variable at package scope can be changed at any time 31 | return true 32 | } 33 | if !gosec.TryResolve(ident, c) { 34 | return true 35 | } 36 | } 37 | } 38 | } 39 | return false 40 | } 41 | 42 | // Match inspects AST nodes to determine if certain net/http methods are called with variable input 43 | func (r *ssrf) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 44 | // Call expression is using http package directly 45 | if node := r.ContainsPkgCallExpr(n, c, false); node != nil { 46 | if r.ResolveVar(node, c) { 47 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 48 | } 49 | } 50 | return nil, nil 51 | } 52 | 53 | // NewSSRFCheck detects cases where HTTP requests are sent 54 | func NewSSRFCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 55 | rule := &ssrf{ 56 | CallList: gosec.NewCallList(), 57 | MetaData: gosec.MetaData{ 58 | ID: id, 59 | What: "Potential HTTP request made with variable url", 60 | Severity: gosec.Medium, 61 | Confidence: gosec.Medium, 62 | }, 63 | } 64 | rule.AddAll("net/http", "Do", "Get", "Head", "Post", "PostForm", "RoundTrip") 65 | return rule, []ast.Node{(*ast.CallExpr)(nil)} 66 | } 67 | -------------------------------------------------------------------------------- /rules/subproc.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "go/types" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type subprocess struct { 25 | gosec.MetaData 26 | gosec.CallList 27 | } 28 | 29 | func (r *subprocess) ID() string { 30 | return r.MetaData.ID 31 | } 32 | 33 | // TODO(gm) The only real potential for command injection with a Go project 34 | // is something like this: 35 | // 36 | // syscall.Exec("/bin/sh", []string{"-c", tainted}) 37 | // 38 | // E.g. Input is correctly escaped but the execution context being used 39 | // is unsafe. For example: 40 | // 41 | // syscall.Exec("echo", "foobar" + tainted) 42 | func (r *subprocess) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 43 | if node := r.ContainsPkgCallExpr(n, c, false); node != nil { 44 | args := node.Args 45 | if r.isContext(n, c) { 46 | args = args[1:] 47 | } 48 | for _, arg := range args { 49 | if ident, ok := arg.(*ast.Ident); ok { 50 | obj := c.Info.ObjectOf(ident) 51 | if _, ok := obj.(*types.Var); ok && !gosec.TryResolve(ident, c) { 52 | return gosec.NewIssue(c, n, r.ID(), "Subprocess launched with variable", gosec.Medium, gosec.High), nil 53 | } 54 | } else if !gosec.TryResolve(arg, c) { 55 | // the arg is not a constant or a variable but instead a function call or os.Args[i] 56 | return gosec.NewIssue(c, n, r.ID(), "Subprocess launched with function call as argument or cmd arguments", gosec.Medium, gosec.High), nil 57 | } 58 | } 59 | } 60 | return nil, nil 61 | } 62 | 63 | // isContext checks whether or not the node is a CommandContext call or not 64 | // Thi is requried in order to skip the first argument from the check. 65 | func (r *subprocess) isContext(n ast.Node, ctx *gosec.Context) bool { 66 | selector, indent, err := gosec.GetCallInfo(n, ctx) 67 | if err != nil { 68 | return false 69 | } 70 | if selector == "exec" && indent == "CommandContext" { 71 | return true 72 | } 73 | return false 74 | } 75 | 76 | // NewSubproc detects cases where we are forking out to an external process 77 | func NewSubproc(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 78 | rule := &subprocess{gosec.MetaData{ID: id}, gosec.NewCallList()} 79 | rule.Add("os/exec", "Command") 80 | rule.Add("os/exec", "CommandContext") 81 | rule.Add("syscall", "Exec") 82 | rule.Add("syscall", "ForkExec") 83 | rule.Add("syscall", "StartProcess") 84 | return rule, []ast.Node{(*ast.CallExpr)(nil)} 85 | } 86 | -------------------------------------------------------------------------------- /rules/tempfiles.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "regexp" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type badTempFile struct { 25 | gosec.MetaData 26 | calls gosec.CallList 27 | args *regexp.Regexp 28 | } 29 | 30 | func (t *badTempFile) ID() string { 31 | return t.MetaData.ID 32 | } 33 | 34 | func (t *badTempFile) Match(n ast.Node, c *gosec.Context) (gi *gosec.Issue, err error) { 35 | if node := t.calls.ContainsPkgCallExpr(n, c, false); node != nil { 36 | if arg, e := gosec.GetString(node.Args[0]); t.args.MatchString(arg) && e == nil { 37 | return gosec.NewIssue(c, n, t.ID(), t.What, t.Severity, t.Confidence), nil 38 | } 39 | } 40 | return nil, nil 41 | } 42 | 43 | // NewBadTempFile detects direct writes to predictable path in temporary directory 44 | func NewBadTempFile(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 45 | calls := gosec.NewCallList() 46 | calls.Add("io/ioutil", "WriteFile") 47 | calls.Add("os", "Create") 48 | return &badTempFile{ 49 | calls: calls, 50 | args: regexp.MustCompile(`^/tmp/.*$|^/var/tmp/.*$`), 51 | MetaData: gosec.MetaData{ 52 | ID: id, 53 | Severity: gosec.Medium, 54 | Confidence: gosec.High, 55 | What: "File creation in shared tmp directory without using ioutil.Tempfile", 56 | }, 57 | }, []ast.Node{(*ast.CallExpr)(nil)} 58 | } 59 | -------------------------------------------------------------------------------- /rules/templates.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | 20 | "github.com/cosmos/gosec/v2" 21 | ) 22 | 23 | type templateCheck struct { 24 | gosec.MetaData 25 | calls gosec.CallList 26 | } 27 | 28 | func (t *templateCheck) ID() string { 29 | return t.MetaData.ID 30 | } 31 | 32 | func (t *templateCheck) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 33 | if node := t.calls.ContainsPkgCallExpr(n, c, false); node != nil { 34 | for _, arg := range node.Args { 35 | if _, ok := arg.(*ast.BasicLit); !ok { // basic lits are safe 36 | return gosec.NewIssue(c, n, t.ID(), t.What, t.Severity, t.Confidence), nil 37 | } 38 | } 39 | } 40 | return nil, nil 41 | } 42 | 43 | // NewTemplateCheck constructs the template check rule. This rule is used to 44 | // find use of templates where HTML/JS escaping is not being used 45 | func NewTemplateCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 46 | 47 | calls := gosec.NewCallList() 48 | calls.Add("html/template", "HTML") 49 | calls.Add("html/template", "HTMLAttr") 50 | calls.Add("html/template", "JS") 51 | calls.Add("html/template", "URL") 52 | return &templateCheck{ 53 | calls: calls, 54 | MetaData: gosec.MetaData{ 55 | ID: id, 56 | Severity: gosec.Medium, 57 | Confidence: gosec.Low, 58 | What: "this method will not auto-escape HTML. Verify data is well formed.", 59 | }, 60 | }, []ast.Node{(*ast.CallExpr)(nil)} 61 | } 62 | -------------------------------------------------------------------------------- /rules/tls.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | //go:generate tlsconfig 16 | 17 | package rules 18 | 19 | import ( 20 | "crypto/tls" 21 | "fmt" 22 | "go/ast" 23 | 24 | "github.com/cosmos/gosec/v2" 25 | ) 26 | 27 | type insecureConfigTLS struct { 28 | gosec.MetaData 29 | MinVersion int16 30 | MaxVersion int16 31 | requiredType string 32 | goodCiphers []string 33 | actualMinVersion int16 34 | actualMaxVersion int16 35 | } 36 | 37 | func (t *insecureConfigTLS) ID() string { 38 | return t.MetaData.ID 39 | } 40 | 41 | func stringInSlice(a string, list []string) bool { 42 | for _, b := range list { 43 | if b == a { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func (t *insecureConfigTLS) processTLSCipherSuites(n ast.Node, c *gosec.Context) *gosec.Issue { 51 | if ciphers, ok := n.(*ast.CompositeLit); ok { 52 | for _, cipher := range ciphers.Elts { 53 | if ident, ok := cipher.(*ast.SelectorExpr); ok { 54 | if !stringInSlice(ident.Sel.Name, t.goodCiphers) { 55 | err := fmt.Sprintf("TLS Bad Cipher Suite: %s", ident.Sel.Name) 56 | return gosec.NewIssue(c, ident, t.ID(), err, gosec.High, gosec.High) 57 | } 58 | } 59 | } 60 | } 61 | return nil 62 | } 63 | 64 | func (t *insecureConfigTLS) processTLSConfVal(n *ast.KeyValueExpr, c *gosec.Context) *gosec.Issue { 65 | if ident, ok := n.Key.(*ast.Ident); ok { 66 | switch ident.Name { 67 | case "InsecureSkipVerify": 68 | if node, ok := n.Value.(*ast.Ident); ok { 69 | if node.Name != "false" { 70 | return gosec.NewIssue(c, n, t.ID(), "TLS InsecureSkipVerify set true.", gosec.High, gosec.High) 71 | } 72 | } else { 73 | // TODO(tk): symbol tab look up to get the actual value 74 | return gosec.NewIssue(c, n, t.ID(), "TLS InsecureSkipVerify may be true.", gosec.High, gosec.Low) 75 | } 76 | 77 | case "PreferServerCipherSuites": 78 | if node, ok := n.Value.(*ast.Ident); ok { 79 | if node.Name == "false" { 80 | return gosec.NewIssue(c, n, t.ID(), "TLS PreferServerCipherSuites set false.", gosec.Medium, gosec.High) 81 | } 82 | } else { 83 | // TODO(tk): symbol tab look up to get the actual value 84 | return gosec.NewIssue(c, n, t.ID(), "TLS PreferServerCipherSuites may be false.", gosec.Medium, gosec.Low) 85 | } 86 | 87 | case "MinVersion": 88 | if ival, ierr := gosec.GetInt(n.Value); ierr == nil { 89 | t.actualMinVersion = (int16)(ival) 90 | } else { 91 | if se, ok := n.Value.(*ast.SelectorExpr); ok { 92 | if pkg, ok := se.X.(*ast.Ident); ok && pkg.Name == "tls" { 93 | t.actualMinVersion = t.mapVersion(se.Sel.Name) 94 | } 95 | } 96 | } 97 | 98 | case "MaxVersion": 99 | if ival, ierr := gosec.GetInt(n.Value); ierr == nil { 100 | t.actualMaxVersion = (int16)(ival) 101 | } else { 102 | if se, ok := n.Value.(*ast.SelectorExpr); ok { 103 | if pkg, ok := se.X.(*ast.Ident); ok && pkg.Name == "tls" { 104 | t.actualMaxVersion = t.mapVersion(se.Sel.Name) 105 | } 106 | } 107 | } 108 | 109 | case "CipherSuites": 110 | if ret := t.processTLSCipherSuites(n.Value, c); ret != nil { 111 | return ret 112 | } 113 | 114 | } 115 | 116 | } 117 | return nil 118 | } 119 | 120 | func (t *insecureConfigTLS) mapVersion(version string) int16 { 121 | var v int16 122 | switch version { 123 | case "VersionTLS13": 124 | v = tls.VersionTLS13 125 | case "VersionTLS12": 126 | v = tls.VersionTLS12 127 | case "VersionTLS11": 128 | v = tls.VersionTLS11 129 | case "VersionTLS10": 130 | v = tls.VersionTLS10 131 | } 132 | return v 133 | } 134 | 135 | func (t *insecureConfigTLS) checkVersion(n ast.Node, c *gosec.Context) *gosec.Issue { 136 | if t.actualMaxVersion == 0 && t.actualMinVersion >= t.MinVersion { 137 | // no warning is generated since the min version is grater than the secure min version 138 | return nil 139 | } 140 | if t.actualMinVersion < t.MinVersion { 141 | return gosec.NewIssue(c, n, t.ID(), "TLS MinVersion too low.", gosec.High, gosec.High) 142 | } 143 | if t.actualMaxVersion < t.MaxVersion { 144 | return gosec.NewIssue(c, n, t.ID(), "TLS MaxVersion too low.", gosec.High, gosec.High) 145 | } 146 | return nil 147 | } 148 | 149 | func (t *insecureConfigTLS) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 150 | if complit, ok := n.(*ast.CompositeLit); ok && complit.Type != nil { 151 | actualType := c.Info.TypeOf(complit.Type) 152 | if actualType != nil && actualType.String() == t.requiredType { 153 | for _, elt := range complit.Elts { 154 | if kve, ok := elt.(*ast.KeyValueExpr); ok { 155 | issue := t.processTLSConfVal(kve, c) 156 | if issue != nil { 157 | return issue, nil 158 | } 159 | } 160 | } 161 | return t.checkVersion(complit, c), nil 162 | } 163 | } 164 | return nil, nil 165 | } 166 | -------------------------------------------------------------------------------- /rules/tls_config.go: -------------------------------------------------------------------------------- 1 | package rules 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | ) 8 | 9 | // NewModernTLSCheck creates a check for Modern TLS ciphers 10 | // DO NOT EDIT - generated by tlsconfig tool 11 | func NewModernTLSCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 12 | return &insecureConfigTLS{ 13 | MetaData: gosec.MetaData{ID: id}, 14 | requiredType: "crypto/tls.Config", 15 | MinVersion: 0x0304, 16 | MaxVersion: 0x0304, 17 | goodCiphers: []string{ 18 | "TLS_AES_128_GCM_SHA256", 19 | "TLS_AES_256_GCM_SHA384", 20 | "TLS_CHACHA20_POLY1305_SHA256", 21 | }, 22 | }, []ast.Node{(*ast.CompositeLit)(nil)} 23 | } 24 | 25 | // NewIntermediateTLSCheck creates a check for Intermediate TLS ciphers 26 | // DO NOT EDIT - generated by tlsconfig tool 27 | func NewIntermediateTLSCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 28 | return &insecureConfigTLS{ 29 | MetaData: gosec.MetaData{ID: id}, 30 | requiredType: "crypto/tls.Config", 31 | MinVersion: 0x0303, 32 | MaxVersion: 0x0304, 33 | goodCiphers: []string{ 34 | "TLS_AES_128_GCM_SHA256", 35 | "TLS_AES_256_GCM_SHA384", 36 | "TLS_CHACHA20_POLY1305_SHA256", 37 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 38 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 39 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 40 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 41 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", 42 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 43 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", 44 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 45 | "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 46 | "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 47 | }, 48 | }, []ast.Node{(*ast.CompositeLit)(nil)} 49 | } 50 | 51 | // NewOldTLSCheck creates a check for Old TLS ciphers 52 | // DO NOT EDIT - generated by tlsconfig tool 53 | func NewOldTLSCheck(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 54 | return &insecureConfigTLS{ 55 | MetaData: gosec.MetaData{ID: id}, 56 | requiredType: "crypto/tls.Config", 57 | MinVersion: 0x0301, 58 | MaxVersion: 0x0304, 59 | goodCiphers: []string{ 60 | "TLS_AES_128_GCM_SHA256", 61 | "TLS_AES_256_GCM_SHA384", 62 | "TLS_CHACHA20_POLY1305_SHA256", 63 | "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256", 64 | "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", 65 | "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384", 66 | "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384", 67 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305", 68 | "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256", 69 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305", 70 | "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256", 71 | "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", 72 | "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", 73 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256", 74 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", 75 | "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", 76 | "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", 77 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", 78 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", 79 | "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA", 80 | "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", 81 | "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", 82 | "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", 83 | "TLS_RSA_WITH_AES_128_GCM_SHA256", 84 | "TLS_RSA_WITH_AES_256_GCM_SHA384", 85 | "TLS_RSA_WITH_AES_128_CBC_SHA256", 86 | "TLS_RSA_WITH_AES_256_CBC_SHA256", 87 | "TLS_RSA_WITH_AES_128_CBC_SHA", 88 | "TLS_RSA_WITH_AES_256_CBC_SHA", 89 | "TLS_RSA_WITH_3DES_EDE_CBC_SHA", 90 | }, 91 | }, []ast.Node{(*ast.CompositeLit)(nil)} 92 | } 93 | -------------------------------------------------------------------------------- /rules/unsafe.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | 20 | "github.com/cosmos/gosec/v2" 21 | ) 22 | 23 | type usingUnsafe struct { 24 | gosec.MetaData 25 | pkg string 26 | calls []string 27 | } 28 | 29 | func (r *usingUnsafe) ID() string { 30 | return r.MetaData.ID 31 | } 32 | 33 | func (r *usingUnsafe) Match(n ast.Node, c *gosec.Context) (gi *gosec.Issue, err error) { 34 | if _, matches := gosec.MatchCallByPackage(n, c, r.pkg, r.calls...); matches { 35 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 36 | } 37 | return nil, nil 38 | } 39 | 40 | // NewUsingUnsafe rule detects the use of the unsafe package. This is only 41 | // really useful for auditing purposes. 42 | func NewUsingUnsafe(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 43 | return &usingUnsafe{ 44 | pkg: "unsafe", 45 | calls: []string{"Alignof", "Offsetof", "Sizeof", "Pointer"}, 46 | MetaData: gosec.MetaData{ 47 | ID: id, 48 | What: "Use of unsafe calls should be audited", 49 | Severity: gosec.Low, 50 | Confidence: gosec.High, 51 | }, 52 | }, []ast.Node{(*ast.CallExpr)(nil)} 53 | } 54 | -------------------------------------------------------------------------------- /rules/weakcrypto.go: -------------------------------------------------------------------------------- 1 | // (c) Copyright 2016 Hewlett Packard Enterprise Development LP 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package rules 16 | 17 | import ( 18 | "go/ast" 19 | "sort" 20 | 21 | "github.com/cosmos/gosec/v2" 22 | ) 23 | 24 | type usesWeakCryptography struct { 25 | gosec.MetaData 26 | blocklist map[string][]string 27 | } 28 | 29 | func (r *usesWeakCryptography) ID() string { 30 | return r.MetaData.ID 31 | } 32 | 33 | func (r *usesWeakCryptography) Match(n ast.Node, c *gosec.Context) (*gosec.Issue, error) { 34 | pkgs := make([]string, 0, len(r.blocklist)) 35 | for pkg := range r.blocklist { 36 | pkgs = append(pkgs, pkg) 37 | } 38 | sort.Slice(pkgs, func(i, j int) bool { return pkgs[i] < pkgs[j] }) 39 | for _, pkg := range pkgs { 40 | if _, matched := gosec.MatchCallByPackage(n, c, pkg, r.blocklist[pkg]...); matched { 41 | return gosec.NewIssue(c, n, r.ID(), r.What, r.Severity, r.Confidence), nil 42 | } 43 | } 44 | return nil, nil 45 | } 46 | 47 | // NewUsesWeakCryptography detects uses of des.* md5.* or rc4.* 48 | func NewUsesWeakCryptography(id string, conf gosec.Config) (gosec.Rule, []ast.Node) { 49 | calls := make(map[string][]string) 50 | calls["crypto/des"] = []string{"NewCipher", "NewTripleDESCipher"} 51 | calls["crypto/md5"] = []string{"New", "Sum"} 52 | calls["crypto/sha1"] = []string{"New", "Sum"} 53 | calls["crypto/rc4"] = []string{"NewCipher"} 54 | rule := &usesWeakCryptography{ 55 | blocklist: calls, 56 | MetaData: gosec.MetaData{ 57 | ID: id, 58 | Severity: gosec.Medium, 59 | Confidence: gosec.High, 60 | What: "Use of weak cryptographic primitive", 61 | }, 62 | } 63 | return rule, []ast.Node{(*ast.CallExpr)(nil)} 64 | } 65 | -------------------------------------------------------------------------------- /testdata/with_cgo_import_generated_code.go: -------------------------------------------------------------------------------- 1 | // Code generated by the test. DO NOT EDIT. 2 | package test 3 | 4 | import "C" 5 | 6 | type foo = C.int 7 | -------------------------------------------------------------------------------- /testdata/with_cgo_import_no_generated_code.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "C" 4 | 5 | type fooC = C.int 6 | -------------------------------------------------------------------------------- /testdata/with_generated_header.go: -------------------------------------------------------------------------------- 1 | // Code generated by a test here. DO NOT EDIT. 2 | 3 | package test 4 | 5 | import "io" 6 | 7 | type rr = io.Reader 8 | -------------------------------------------------------------------------------- /testdata/with_regular_code_comment_about_generated.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | // This is a comment about "// Code generated by. DO NOT EDIT." 4 | -------------------------------------------------------------------------------- /testdata/without_generated_header.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import "io" 4 | 5 | type r = io.Reader 6 | -------------------------------------------------------------------------------- /testutils/log.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | ) 7 | 8 | // NewLogger returns a logger and the buffer that it will be written to 9 | func NewLogger() (*log.Logger, *bytes.Buffer) { 10 | var buf bytes.Buffer 11 | return log.New(&buf, "", log.Lshortfile), &buf 12 | } 13 | -------------------------------------------------------------------------------- /testutils/pkg.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "fmt" 5 | "go/build" 6 | "io/ioutil" 7 | "log" 8 | "os" 9 | "path" 10 | "sort" 11 | "strings" 12 | 13 | "github.com/cosmos/gosec/v2" 14 | "golang.org/x/tools/go/packages" 15 | ) 16 | 17 | type buildObj struct { 18 | pkg *build.Package 19 | config *packages.Config 20 | pkgs []*packages.Package 21 | } 22 | 23 | // TestPackage is a mock package for testing purposes 24 | type TestPackage struct { 25 | Path string 26 | Files map[string]string 27 | onDisk bool 28 | build *buildObj 29 | } 30 | 31 | // NewTestPackage will create a new and empty package. Must call Close() to cleanup 32 | // auxiliary files 33 | func NewTestPackage() *TestPackage { 34 | workingDir, err := ioutil.TempDir("", "gosecs_test") 35 | if err != nil { 36 | return nil 37 | } 38 | 39 | return &TestPackage{ 40 | Path: workingDir, 41 | Files: make(map[string]string), 42 | onDisk: false, 43 | build: nil, 44 | } 45 | } 46 | 47 | // AddFile inserts the filename and contents into the package contents 48 | func (p *TestPackage) AddFile(filename, content string) { 49 | p.Files[path.Join(p.Path, filename)] = content 50 | } 51 | 52 | func (p *TestPackage) write() error { 53 | if p.onDisk { 54 | return nil 55 | } 56 | filenames := make([]string, 0, len(p.Files)) 57 | for filename := range p.Files { 58 | filenames = append(filenames, filename) 59 | } 60 | sort.Slice(filenames, func(i, j int) bool { return filenames[i] < filenames[j] }) 61 | for _, filename := range filenames { 62 | if e := ioutil.WriteFile(filename, []byte(p.Files[filename]), 0644); e != nil { 63 | return e 64 | } // #nosec G306 65 | } 66 | p.onDisk = true 67 | return nil 68 | } 69 | 70 | // Build ensures all files are persisted to disk and built 71 | func (p *TestPackage) Build() error { 72 | if p.build != nil { 73 | return nil 74 | } 75 | if err := p.write(); err != nil { 76 | return err 77 | } 78 | basePackage, err := build.Default.ImportDir(p.Path, build.ImportComment) 79 | if err != nil { 80 | return err 81 | } 82 | 83 | var packageFiles []string 84 | for _, filename := range basePackage.GoFiles { 85 | packageFiles = append(packageFiles, path.Join(p.Path, filename)) 86 | } 87 | 88 | conf := &packages.Config{ 89 | Mode: gosec.LoadMode, 90 | Tests: false, 91 | } 92 | pkgs, err := packages.Load(conf, packageFiles...) 93 | if err != nil { 94 | return err 95 | } 96 | p.build = &buildObj{ 97 | pkg: basePackage, 98 | config: conf, 99 | pkgs: pkgs, 100 | } 101 | return nil 102 | } 103 | 104 | // CreateContext builds a context out of supplied package context 105 | func (p *TestPackage) CreateContext(filename string) *gosec.Context { 106 | if err := p.Build(); err != nil { 107 | log.Fatal(err) 108 | return nil 109 | } 110 | 111 | for _, pkg := range p.build.pkgs { 112 | for _, file := range pkg.Syntax { 113 | pkgFile := pkg.Fset.File(file.Pos()).Name() 114 | strip := fmt.Sprintf("%s%c", p.Path, os.PathSeparator) 115 | pkgFile = strings.TrimPrefix(pkgFile, strip) 116 | if pkgFile == filename { 117 | ctx := &gosec.Context{ 118 | FileSet: pkg.Fset, 119 | Root: file, 120 | Config: gosec.NewConfig(), 121 | Info: pkg.TypesInfo, 122 | Pkg: pkg.Types, 123 | Imports: gosec.NewImportTracker(), 124 | PassedValues: make(map[string]interface{}), 125 | } 126 | ctx.Imports.TrackPackages(ctx.Pkg.Imports()...) 127 | return ctx 128 | } 129 | } 130 | } 131 | return nil 132 | } 133 | 134 | // Close will delete the package and all files in that directory 135 | func (p *TestPackage) Close() { 136 | if p.onDisk { 137 | err := os.RemoveAll(p.Path) 138 | if err != nil { 139 | log.Fatal(err) 140 | } 141 | } 142 | } 143 | 144 | // Pkgs returns the current built packages 145 | func (p *TestPackage) Pkgs() []*packages.Package { 146 | if p.build != nil { 147 | return p.build.pkgs 148 | } 149 | return []*packages.Package{} 150 | } 151 | -------------------------------------------------------------------------------- /testutils/visitor.go: -------------------------------------------------------------------------------- 1 | package testutils 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "github.com/cosmos/gosec/v2" 7 | ) 8 | 9 | // MockVisitor is useful for stubbing out ast.Visitor with callback 10 | // and looking for specific conditions to exist. 11 | type MockVisitor struct { 12 | Context *gosec.Context 13 | Callback func(n ast.Node, ctx *gosec.Context) bool 14 | } 15 | 16 | // NewMockVisitor creates a new empty struct, the Context and 17 | // Callback must be set manually. See call_list_test.go for an example. 18 | func NewMockVisitor() *MockVisitor { 19 | return &MockVisitor{} 20 | } 21 | 22 | // Visit satisfies the ast.Visitor interface 23 | func (v *MockVisitor) Visit(n ast.Node) ast.Visitor { 24 | if v.Callback(n, v.Context) { 25 | return v 26 | } 27 | return nil 28 | } 29 | --------------------------------------------------------------------------------