diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..78ff53f1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,124 @@ +# Contributing + +> This guide is for internal Snyk contributors with write access to this repository. If you are an external contributor, before working on any contributions, please first [contact support](https://support.snyk.io) to discuss the issue or feature request with us. + +## Prerequisites + +You will need the following software installed: + +- Git +- Go + - Use whichever version is in [`go.mod`](./go.mod). + +Open a terminal and make sure they are available. + +```sh +git --version +go version +``` + +## Setting up + +Clone this repository with git. + +```sh +git clone git@github.com:snyk/code-client-go.git +cd code-client-go +``` + +You will now be on our `main` branch. You should never commit to this branch, but you should keep it up-to-date to ensure you have the latest changes. + +```sh +git fetch +git pull --ff-only +``` + +## Running tests + +To run the tests run: + +```sh +make test +``` + +## Code ownership + +For current ownership assignments, see: [CODEOWNERS](./.github/CODEOWNERS). + +To avoid mixing ownership into a single file, move team-specific logic into separate files. To reduce blockers and save time, design with ownership in mind. + +## Code formatting + +To ensure your changes follow formatting guidelines, you can run the linter. + +``` +make lint +``` + +To fix various issues automatically you can run the following: + +``` +make format +``` + +You will need to fix any remaining issues manually. + +## Creating a branch + +Create a new branch before making any changes. Make sure to give it a descriptive name so that you can find it later. + +```sh +git checkout -b type/topic +``` + +For example: + +```sh +git checkout -b docs/contributing +``` + +## Creating commits + +Each commit must provide some benefit on its own without breaking the release pipeline. + +For larger changes, break down each step into multiple commits so that it's easy to review in pull requests and git history. + +Commits must follow [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) structure: + +``` +type: summary of your changes + +reasoning behind your changes +``` + +For example: + +``` +docs: update contributing guide + +We often get questions on how to contribute to this repo. What versions to use, what the workflow is, and so on. This change updates our CONTRIBUTING guide to answer those types of questions. +``` + +### Commit types + +The commit type is used to summarize intent and to automate various steps. + +| Type | Description | +| ---------- | ----------------------------------------------- | +| `feat` | A new user-facing feature. | +| `fix` | A bug fix for an existing feature. | +| `refactor` | Changes which do not affect existing features. | +| `test` | Changes to tests for existing features. | +| `docs` | Changes to documentation for existing features. | +| `chore` | Build, workflow and pipeline changes. | +| `revert` | Reverting a previous commit. | + +## Pushing changes + +Once you have committed your changes, review them locally, then push them to GitHub. + +``` +git push +``` + +Do not hold onto your changes for too long. Commit and push frequently and create a pull request as soon as possible for backup and visibility. diff --git a/go.mod b/go.mod index c0c492cf..c75b3755 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,11 @@ module github.com/snyk/code-client-go go 1.21.7 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..fa4b6e68 --- /dev/null +++ b/go.sum @@ -0,0 +1,10 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sarif_types.go b/sarif_types.go new file mode 100644 index 00000000..a6a2da1c --- /dev/null +++ b/sarif_types.go @@ -0,0 +1,178 @@ +/* + * © 2022 Snyk Limited All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package code_client_go + +type SarifResponse struct { + Type string `json:"type"` + Progress float64 `json:"progress"` + Status string `json:"status"` + Timing struct { + FetchingCode int `json:"fetchingCode"` + Queue int `json:"queue"` + Analysis int `json:"analysis"` + } `json:"timing"` + Coverage []struct { + Files int `json:"files"` + IsSupported bool `json:"isSupported"` + Lang string `json:"lang"` + } `json:"coverage"` + Sarif struct { + Schema string `json:"$schema"` + Version string `json:"version"` + Runs []Run `json:"runs"` + } `json:"sarif"` +} + +type region struct { + StartLine int `json:"startLine"` + EndLine int `json:"endLine"` + StartColumn int `json:"startColumn"` + EndColumn int `json:"endColumn"` +} + +type ArtifactLocation struct { + URI string `json:"uri"` + URIBaseID string `json:"uriBaseId"` +} + +type PhysicalLocation struct { + ArtifactLocation ArtifactLocation `json:"ArtifactLocation"` + Region region `json:"region"` +} + +type Location struct { + ID int `json:"id"` + PhysicalLocation PhysicalLocation `json:"PhysicalLocation"` +} + +type ThreadFlowLocation struct { + Location Location `json:"Location"` +} + +type ThreadFlow struct { + Locations []ThreadFlowLocation `json:"locations"` +} + +type CodeFlow struct { + ThreadFlows []ThreadFlow `json:"threadFlows"` +} + +type ResultMessage struct { + Text string `json:"text"` + Markdown string `json:"markdown"` + Arguments []string `json:"arguments"` +} + +type Fingerprints struct { + Num0 string `json:"0"` + Num1 string `json:"1"` +} + +type ResultProperties struct { + PriorityScore int `json:"priorityScore"` + PriorityScoreFactors []struct { + Label bool `json:"label"` + Type string `json:"type"` + } `json:"priorityScoreFactors"` + IsAutofixable bool `json:"isAutofixable"` +} + +type Result struct { + RuleID string `json:"ruleId"` + RuleIndex int `json:"ruleIndex"` + Level string `json:"level"` + Message ResultMessage `json:"message"` + Locations []Location `json:"locations"` + Fingerprints Fingerprints `json:"Fingerprints"` + CodeFlows []CodeFlow `json:"codeFlows"` + Properties ResultProperties `json:"properties"` +} + +type ExampleCommitFix struct { + CommitURL string `json:"commitURL"` + Lines []struct { + Line string `json:"line"` + LineNumber int `json:"lineNumber"` + LineChange string `json:"lineChange"` + } `json:"lines"` +} + +type Help struct { + Markdown string `json:"markdown"` + Text string `json:"text"` +} + +type RuleProperties struct { + Tags []string `json:"tags"` + ShortDescription struct { + Text string `json:"text"` + } `json:"ShortDescription"` + + Help struct { + Markdown string `json:"markdown"` + Text string `json:"text"` + } `json:"Help"` + + Categories []string `json:"categories"` + ExampleCommitFixes []ExampleCommitFix `json:"exampleCommitFixes"` + ExampleCommitDescriptions []string `json:"exampleCommitDescriptions"` + Precision string `json:"precision"` + RepoDatasetSize int `json:"repoDatasetSize"` + Cwe []string `json:"cwe"` +} + +type DefaultConfiguration struct { + Level string `json:"level"` +} + +type ShortDescription struct { + Text string `json:"text"` +} + +type Rule struct { + ID string `json:"id"` + Name string `json:"name"` + ShortDescription ShortDescription `json:"ShortDescription"` + DefaultConfiguration DefaultConfiguration `json:"DefaultConfiguration"` + Help Help `json:"Help"` + Properties RuleProperties `json:"properties"` +} + +type Driver struct { + Name string `json:"name"` + SemanticVersion string `json:"semanticVersion"` + Version string `json:"version"` + Rules []Rule `json:"rules"` +} + +type Tool struct { + Driver Driver `json:"Driver"` +} + +type runProperties struct { + Coverage []struct { + Files int `json:"files"` + IsSupported bool `json:"isSupported"` + Lang string `json:"lang"` + } `json:"coverage"` +} + +type Run struct { + Tool Tool `json:"Tool"` + Results []Result `json:"results"` + Properties runProperties `json:"RuleProperties"` +} diff --git a/scan.go b/scan.go new file mode 100644 index 00000000..1b3d352d --- /dev/null +++ b/scan.go @@ -0,0 +1,613 @@ +/* + * © 2022 Snyk Limited All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package code_client_go + +import ( + "encoding/json" +) + +var fakeResponse = `{ + "type": "sarif", + "progress": 1, + "status": "COMPLETE", + "timing": { + "fetchingCode": 2, + "queue": 22, + "analysis": 3015 + }, + "coverage": [ + { + "files": 1, + "isSupported": false, + "lang": "DIGITAL CommandData Language" + }, + { + "files": 1, + "isSupported": true, + "lang": "Java" + } + ], + "sarif": { + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "Tool": { + "Driver": { + "name": "SnykCode", + "semanticVersion": "1.0.0", + "version": "1.0.0", + "rules": [ + { + "id": "java/DontUsePrintStackTrace", + "name": "DontUsePrintStackTrace", + "ShortDescription": { + "text": "DontUsePrintStackTrace" + }, + "DefaultConfiguration": { + "level": "note" + }, + "Help": { + "markdown": "", + "text": "" + }, + "properties": { + "tags": [ + "java", + "maintenance", + "bug", + "logging", + "exception", + "error" + ], + "categories": [ + "Defect" + ], + "exampleCommitFixes": [ + { + "commitURL": "https://github.com/apache/flink/commit/5d7c5620804eddd59206b24c87ffc89c12fd1184?diff=split#diff-86ec3e3884662ba3b5f4bb5050221fd6L94", + "lines": [ + { + "line": "try {", + "lineNumber": 101, + "lineChange": "none" + }, + { + "line": " newCopy.read(dis);", + "lineNumber": 102, + "lineChange": "none" + }, + { + "line": "} catch (IOException e) {", + "lineNumber": 103, + "lineChange": "none" + }, + { + "line": " e.printStackTrace();", + "lineNumber": 94, + "lineChange": "removed" + }, + { + "line": " LOG.error(e);", + "lineNumber": 104, + "lineChange": "added" + }, + { + "line": "}", + "lineNumber": 105, + "lineChange": "none" + } + ] + }, + { + "commitURL": "https://github.com/rtr-nettest/open-rmbt/commit/0fa9d5547c5300cf8162b8f31a40aea6847a5c32?diff=split#diff-7e23eb1aa3b7b4d5db89bfd2860277e5L75", + "lines": [ + { + "line": " }", + "lineNumber": 111, + "lineChange": "none" + }, + { + "line": "}", + "lineNumber": 112, + "lineChange": "none" + }, + { + "line": "catch (Exception e) {", + "lineNumber": 113, + "lineChange": "none" + }, + { + "line": " e.printStackTrace();", + "lineNumber": 75, + "lineChange": "removed" + }, + { + "line": " error(e, 0);", + "lineNumber": 114, + "lineChange": "added" + }, + { + "line": " state.set(JobState.ERROR);", + "lineNumber": 115, + "lineChange": "added" + }, + { + "line": "}", + "lineNumber": 116, + "lineChange": "none" + }, + { + "line": "finally {", + "lineNumber": 117, + "lineChange": "none" + } + ] + }, + { + "commitURL": "https://github.com/wso2/developer-studio/commit/cfd84b83349e67de4b0239733bc6ed01287856b7?diff=split#diff-645425e844adc2eab8197719cbb2fe8dL285", + "lines": [ + { + "line": " } catch (SAXException e) {", + "lineNumber": 282, + "lineChange": "none" + }, + { + "line": " e.printStackTrace();", + "lineNumber": 283, + "lineChange": "removed" + }, + { + "line": " log.error(e);", + "lineNumber": 282, + "lineChange": "added" + }, + { + "line": " } catch (IOException e) {", + "lineNumber": 284, + "lineChange": "none" + }, + { + "line": " e.printStackTrace();", + "lineNumber": 285, + "lineChange": "removed" + }, + { + "line": " log.error(e);", + "lineNumber": 284, + "lineChange": "added" + }, + { + "line": " }", + "lineNumber": 286, + "lineChange": "none" + }, + { + "line": "}", + "lineNumber": 287, + "lineChange": "none" + } + ] + } + ], + "exampleCommitDescriptions": [ + "improve logging and testing", + "more tests, exceptions", + "log errors to the log file" + ], + "precision": "very-high", + "repoDatasetSize": 5854 + } + }, + { + "id": "java/catchingInterruptedExceptionWithoutInterrupt", + "name": "catchingInterruptedExceptionWithoutInterrupt", + "ShortDescription": { + "text": "catchingInterruptedExceptionWithoutInterrupt" + }, + "DefaultConfiguration": { + "level": "warning" + }, + "Help": { + "markdown": "", + "text": "" + }, + "properties": { + "tags": [ + "java", + "bug", + "maintenance", + "import", + "remoting.jar", + "overwrite" + ], + "categories": [ + "Defect" + ], + "exampleCommitFixes": [ + { + "commitURL": "https://github.com/markusfisch/ShaderEditor/commit/ea90be086b71df55a675a4a75d35c6f294a634a9?diff=split#diff-924648dd89d8c5ea66b90291ac693c9aL739", + "lines": [ + { + "line": " Thread.sleep(100);", + "lineNumber": 736, + "lineChange": "none" + }, + { + "line": " }", + "lineNumber": 737, + "lineChange": "none" + }, + { + "line": "} catch (InterruptedException e) {", + "lineNumber": 738, + "lineChange": "none" + }, + { + "line": " // thread got interrupted, ignore that", + "lineNumber": 739, + "lineChange": "removed" + }, + { + "line": " Thread.currentThread().interrupt();", + "lineNumber": 739, + "lineChange": "added" + }, + { + "line": "}", + "lineNumber": 740, + "lineChange": "none" + } + ] + }, + { + "commitURL": "https://github.com/yegor256/rexsl/commit/c147bbb780882cdf8e62e4de46b8f99b86d94a5c?diff=split#diff-43fdfda5b43f9f592cb0e8fc194b12ddL64", + "lines": [ + { + "line": " // @checkstyle MagicNumber (1 line)", + "lineNumber": 61, + "lineChange": "none" + }, + { + "line": " Thread.sleep(1000);", + "lineNumber": 62, + "lineChange": "none" + }, + { + "line": " } catch (java.lang.InterruptedException ex) {", + "lineNumber": 63, + "lineChange": "none" + }, + { + "line": " container.stop();", + "lineNumber": 64, + "lineChange": "none" + }, + { + "line": " Thread.currentThread().interrupt();", + "lineNumber": 65, + "lineChange": "added" + }, + { + "line": " }", + "lineNumber": 66, + "lineChange": "none" + }, + { + "line": "}", + "lineNumber": 67, + "lineChange": "none" + } + ] + }, + { + "commitURL": "https://github.com/apache/tomcat/commit/c6bd6f4afbf24c23b3ff03ec652f7e4524694a1e?diff=split#diff-7fc346c0b69fcfdc8e4ad44afc3b345fL85", + "lines": [ + { + "line": " configureTask(worker);", + "lineNumber": 82, + "lineChange": "none" + }, + { + "line": " } else {", + "lineNumber": 83, + "lineChange": "none" + }, + { + "line": " try { mutex.wait(); } catch ( java.lang.InterruptedException x ) {Thread.interrupted();}", + "lineNumber": 84, + "lineChange": "removed" + }, + { + "line": " try {", + "lineNumber": 84, + "lineChange": "added" + }, + { + "line": " mutex.wait();", + "lineNumber": 85, + "lineChange": "added" + }, + { + "line": " } catch (java.lang.InterruptedException x) {", + "lineNumber": 86, + "lineChange": "added" + }, + { + "line": " Thread.currentThread().interrupt();", + "lineNumber": 87, + "lineChange": "added" + }, + { + "line": " }", + "lineNumber": 88, + "lineChange": "added" + }, + { + "line": " }", + "lineNumber": 89, + "lineChange": "none" + }, + { + "line": "}//while", + "lineNumber": 90, + "lineChange": "none" + } + ] + } + ], + "exampleCommitDescriptions": [ + "Clean up import statements in java code.", + "Overwrite remoting.jar only when necessary." + ], + "precision": "very-high", + "repoDatasetSize": 26 + } + } + ] + } + }, + "results": [ + { + "ruleId": "java/DontUsePrintStackTrace", + "ruleIndex": 0, + "level": "note", + "message": { + "text": "Printing the stack trace of java.lang.InterruptedException. Production code should not use printStackTrace.", + "markdown": "Printing the stack trace of {0}. Production code should not use {1}. {2}", + "arguments": [ + "[java.lang.InterruptedException](0)", + "[printStackTrace](1)(2)", + "[This is a test argument](3)" + ] + }, + "locations": [ + { + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 6, + "endLine": 6, + "startColumn": 7, + "endColumn": 7 + } + } + } + ], + "Fingerprints": { + "0": "35bc91513238a0a06af1824552fb3f838201f6fbbf1d76632b2604242e838d20", + "1": "c2e08f55.1333c445.d1699128.15932eef.606b2add.34c3b532.4a752797.e9000d02.c2e08f55.1333c445.cd271e66.e22980a8.d31a8364.2f2c7742.4a752797.54d46e25" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "Location": { + "id": 0, + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 5, + "endLine": 5, + "startColumn": 14, + "endColumn": 33 + } + } + } + }, + { + "Location": { + "id": 1, + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 6, + "endLine": 6, + "startColumn": 9, + "endColumn": 23 + } + } + } + }, + { + "Location": { + "id": 2, + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 10, + "endLine": 10, + "startColumn": 10, + "endColumn": 10 + } + } + } + }, + { + "Location": { + "id": 3, + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 20, + "endLine": 20, + "startColumn": 20, + "endColumn": 20 + } + } + } + } + ] + } + ] + } + ], + "properties": { + "priorityScore": 550, + "priorityScoreFactors": [ + { + "label": true, + "type": "hotFileSource" + }, + { + "label": true, + "type": "fixExamples" + }, + { + "label": true, + "type": "commonlyFixed" + } + ] + } + }, + { + "ruleId": "java/catchingInterruptedExceptionWithoutInterrupt", + "ruleIndex": 1, + "level": "warning", + "message": { + "text": "Either rethrow this java.lang.InterruptedException or set the interrupted flag on the current thread with 'Thread.currentThread().interrupt()'. Otherwise the information that the current thread was interrupted will be lost.", + "markdown": "Either rethrow this {0} or set the interrupted flag on the current thread with 'Thread.currentThread().interrupt()'. Otherwise the information that the current thread was interrupted will be lost.", + "arguments": [ + "[java.lang.InterruptedException](0)" + ] + }, + "locations": [ + { + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 5, + "endLine": 5, + "startColumn": 7, + "endColumn": 35 + } + } + } + ], + "Fingerprints": { + "0": "4ee04cfd17e0a8bee301d4741b26962f0a9630ac811ab48c06513857c3319f4c", + "1": "c2e08f55.1333c445.cd271e66.e22980a8.d31a8364.2f2c7742.4a752797.54d46e25.c2e08f55.1333c445.cd271e66.e22980a8.d31a8364.2f2c7742.4a752797.54d46e25" + }, + "codeFlows": [ + { + "threadFlows": [ + { + "locations": [ + { + "Location": { + "id": 0, + "PhysicalLocation": { + "ArtifactLocation": { + "uri": "src/main.ts", + "uriBaseId": "dummy" + }, + "region": { + "startLine": 5, + "endLine": 5, + "startColumn": 14, + "endColumn": 33 + } + } + } + } + ] + } + ] + } + ], + "properties": { + "priorityScore": 600, + "priorityScoreFactors": [ + { + "label": true, + "type": "hotFileSource" + }, + { + "label": true, + "type": "fixExamples" + } + ] + } + } + ], + "properties": { + "coverage": [ + { + "files": 1, + "isSupported": false, + "lang": "DIGITAL CommandData Language" + }, + { + "files": 1, + "isSupported": true, + "lang": "Java" + } + ] + } + } + ] + } +}` + +func UploadAndAnalyze() (*SarifResponse, error) { + var response SarifResponse + err := json.Unmarshal([]byte(fakeResponse), &response) + if err != nil { + return nil, err + } + return &response, nil +} diff --git a/scan_test.go b/scan_test.go new file mode 100644 index 00000000..e5cdeaed --- /dev/null +++ b/scan_test.go @@ -0,0 +1,14 @@ +package code_client_go_test + +import ( + "github.com/snyk/code-client-go" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "testing" +) + +func TestUploadAndAnalyze(t *testing.T) { + actual, err := code_client_go.UploadAndAnalyze() + require.NoError(t, err) + assert.Equal(t, "COMPLETE", actual.Status) +}