mirror of
https://github.com/securego/gosec.git
synced 2024-12-25 03:55:54 +00:00
Generate the SARIF types, handle taxonomies and separate responsibilities
This commit is contained in:
parent
0fa5d0b2d6
commit
cc83d4c922
13 changed files with 2011 additions and 279 deletions
16
README.md
16
README.md
|
@ -311,6 +311,22 @@ You can build the binary with:
|
|||
make
|
||||
```
|
||||
|
||||
### Note on Sarif Types Generation
|
||||
|
||||
Install the tool with :
|
||||
```bash
|
||||
go get -u github.com/a-h/generate/cmd/schema-generate
|
||||
```
|
||||
|
||||
Then generate the types with :
|
||||
```bash
|
||||
schema-generate -i sarif-schema-2.1.0.json -o mypath/types.go
|
||||
```
|
||||
|
||||
Most of the MarshallJSON/UnmarshalJSON are removed except the one for PropertyBag which is handy to inline the additionnal properties. The rest can be removed.
|
||||
The URI,ID, UUID, GUID were renamed so it fits the Golang convention defined [here](https://github.com/golang/lint/blob/master/lint.go#L700)
|
||||
|
||||
|
||||
### Tests
|
||||
|
||||
You can run all unit tests using:
|
||||
|
|
109
cwe/data.go
Normal file
109
cwe/data.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
package cwe
|
||||
|
||||
var data = map[string]Weakness{
|
||||
"118": {
|
||||
ID: "118",
|
||||
Description: "The software does not restrict or incorrectly restricts operations within the boundaries of a resource that is accessed using an index or pointer, such as memory or files.",
|
||||
Name: "Incorrect Access of Indexable Resource ('Range Error')",
|
||||
},
|
||||
"190": {
|
||||
ID: "190",
|
||||
Description: "The software performs a calculation that can produce an integer overflow or wraparound, when the logic assumes that the resulting value will always be larger than the original value. This can introduce other weaknesses when the calculation is used for resource management or execution control.",
|
||||
Name: "Integer Overflow or Wraparound",
|
||||
},
|
||||
"200": {
|
||||
ID: "200",
|
||||
Description: "The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.",
|
||||
Name: "Exposure of Sensitive Information to an Unauthorized Actor",
|
||||
},
|
||||
"22": {
|
||||
ID: "22",
|
||||
Description: "The software uses external input to construct a pathname that is intended to identify a file or directory that is located underneath a restricted parent directory, but the software does not properly neutralize special elements within the pathname that can cause the pathname to resolve to a location that is outside of the restricted directory.",
|
||||
Name: "Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal')",
|
||||
},
|
||||
"242": {
|
||||
ID: "242",
|
||||
Description: "The program calls a function that can never be guaranteed to work safely.",
|
||||
Name: "Use of Inherently Dangerous Function",
|
||||
},
|
||||
"276": {
|
||||
ID: "276",
|
||||
Description: "During installation, installed file permissions are set to allow anyone to modify those files.",
|
||||
Name: "Incorrect Default Permissions",
|
||||
},
|
||||
"295": {
|
||||
ID: "295",
|
||||
Description: "The software does not validate, or incorrectly validates, a certificate.",
|
||||
Name: "Improper Certificate Validation",
|
||||
},
|
||||
"310": {
|
||||
ID: "310",
|
||||
Description: "Weaknesses in this category are related to the design and implementation of data confidentiality and integrity. Frequently these deal with the use of encoding techniques, encryption libraries, and hashing algorithms. The weaknesses in this category could lead to a degradation of the quality data if they are not addressed.",
|
||||
Name: "Cryptographic Issues",
|
||||
},
|
||||
"322": {
|
||||
ID: "322",
|
||||
Description: "The software performs a key exchange with an actor without verifying the identity of that actor.",
|
||||
Name: "Key Exchange without Entity Authentication",
|
||||
},
|
||||
"326": {
|
||||
ID: "326",
|
||||
Description: "The software stores or transmits sensitive data using an encryption scheme that is theoretically sound, but is not strong enough for the level of protection required.",
|
||||
Name: "Inadequate Encryption Strength",
|
||||
},
|
||||
"327": {
|
||||
ID: "327",
|
||||
Description: "The use of a broken or risky cryptographic algorithm is an unnecessary risk that may result in the exposure of sensitive information.",
|
||||
Name: "Use of a Broken or Risky Cryptographic Algorithm",
|
||||
},
|
||||
"338": {
|
||||
ID: "338",
|
||||
Description: "The product uses a Pseudo-Random Number Generator (PRNG) in a security context, but the PRNG's algorithm is not cryptographically strong.",
|
||||
Name: "Use of Cryptographically Weak Pseudo-Random Number Generator (PRNG)",
|
||||
},
|
||||
"377": {
|
||||
ID: "377",
|
||||
Description: "Creating and using insecure temporary files can leave application and system data vulnerable to attack.",
|
||||
Name: "Insecure Temporary File",
|
||||
},
|
||||
"409": {
|
||||
ID: "409",
|
||||
Description: "The software does not handle or incorrectly handles a compressed input with a very high compression ratio that produces a large output.",
|
||||
Name: "Improper Handling of Highly Compressed Data (Data Amplification)",
|
||||
},
|
||||
"703": {
|
||||
ID: "703",
|
||||
Description: "The software does not properly anticipate or handle exceptional conditions that rarely occur during normal operation of the software.",
|
||||
Name: "Improper Check or Handling of Exceptional Conditions",
|
||||
},
|
||||
"78": {
|
||||
ID: "78",
|
||||
Description: "The software constructs all or part of an OS command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended OS command when it is sent to a downstream component.",
|
||||
Name: "Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection')",
|
||||
},
|
||||
"79": {
|
||||
ID: "79",
|
||||
Description: "The software does not neutralize or incorrectly neutralizes user-controllable input before it is placed in output that is used as a web page that is served to other users.",
|
||||
Name: "Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')",
|
||||
},
|
||||
"798": {
|
||||
ID: "798",
|
||||
Description: "The software contains hard-coded credentials, such as a password or cryptographic key, which it uses for its own inbound authentication, outbound communication to external components, or encryption of internal data.",
|
||||
Name: "Use of Hard-coded Credentials",
|
||||
},
|
||||
"88": {
|
||||
ID: "88",
|
||||
Description: "The software constructs a string for a command to executed by a separate component\nin another control sphere, but it does not properly delimit the\nintended arguments, options, or switches within that command string.",
|
||||
Name: "Improper Neutralization of Argument Delimiters in a Command ('Argument Injection')",
|
||||
},
|
||||
"89": {
|
||||
ID: "89",
|
||||
Description: "The software constructs all or part of an SQL command using externally-influenced input from an upstream component, but it does not neutralize or incorrectly neutralizes special elements that could modify the intended SQL command when it is sent to a downstream component.",
|
||||
Name: "Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')",
|
||||
},
|
||||
}
|
||||
|
||||
//Get Retrieves a CWE weakness by it's id
|
||||
func Get(id string) Weakness {
|
||||
return data[id]
|
||||
}
|
17
cwe/types.go
Normal file
17
cwe/types.go
Normal file
|
@ -0,0 +1,17 @@
|
|||
package cwe
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// Weakness defines a CWE weakness based on http://cwe.mitre.org/data/xsd/cwe_schema_v6.4.xsd
|
||||
type Weakness struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
}
|
||||
|
||||
//URL Expose the CWE URL
|
||||
func (w *Weakness) URL() string {
|
||||
return fmt.Sprintf("https://cwe.mitre.org/data/definitions/%s.html", w.ID)
|
||||
}
|
12
formatter/types.go
Normal file
12
formatter/types.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package formatter
|
||||
|
||||
import (
|
||||
"github.com/securego/gosec/v2"
|
||||
)
|
||||
|
||||
//ReportInfo this is report information
|
||||
type ReportInfo struct {
|
||||
Errors map[string][]gosec.Error `json:"Golang errors"`
|
||||
Issues []*gosec.Issue
|
||||
Stats *gosec.Metrics
|
||||
}
|
3
go.mod
3
go.mod
|
@ -1,13 +1,12 @@
|
|||
module github.com/securego/gosec/v2
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.1.1
|
||||
github.com/gookit/color v1.4.2
|
||||
github.com/lib/pq v1.10.1 // indirect
|
||||
github.com/mozilla/tls-observatory v0.0.0-20210209181001-cf43108d6880
|
||||
github.com/nbutton23/zxcvbn-go v0.0.0-20210217022336-fa2cb2858354
|
||||
github.com/onsi/ginkgo v1.16.1
|
||||
github.com/onsi/gomega v1.11.0
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e // indirect
|
||||
golang.org/x/mod v0.4.1 // indirect
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110 // indirect
|
||||
golang.org/x/text v0.3.5 // indirect
|
||||
|
|
7
go.sum
7
go.sum
|
@ -160,6 +160,7 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
|
|||
github.com/google/trillian v1.3.11/go.mod h1:0tPraVHrSDkA3BO6vKX67zgLXs6SsOAbHEivX+9mPgw=
|
||||
github.com/google/uuid v0.0.0-20161128191214-064e2069ce9c/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
|
||||
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
|
||||
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
|
||||
|
@ -215,9 +216,8 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/letsencrypt/pkcs11key/v4 v4.0.0/go.mod h1:EFUvBDay26dErnNb70Nd0/VW3tJiIbETBPTl9ATXQag=
|
||||
github.com/lib/pq v1.8.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.9.0 h1:L8nSXQQzAYByakOFMTwpjRoHsMJklur4Gi59b6VivR8=
|
||||
github.com/lib/pq v1.9.0/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/lib/pq v1.10.1 h1:6VXZrLU0jHBYyAqrSPa+MgPfnSvTPuMgK+k0o5kVFWo=
|
||||
github.com/lib/pq v1.10.1/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE=
|
||||
|
@ -366,9 +366,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e h1:8foAy0aoO5GkqCvAEJ4VC4P3zksTg4X4aJCDpZzmgQI=
|
||||
golang.org/x/crypto v0.0.0-20210503195802-e9a32991a82e/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
|
||||
|
|
|
@ -29,6 +29,8 @@ import (
|
|||
|
||||
color "github.com/gookit/color"
|
||||
"github.com/securego/gosec/v2"
|
||||
"github.com/securego/gosec/v2/formatter"
|
||||
"github.com/securego/gosec/v2/sarif"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
|
@ -76,16 +78,10 @@ Golang errors in file: [{{ $filePath }}]:
|
|||
|
||||
`
|
||||
|
||||
type reportInfo struct {
|
||||
Errors map[string][]gosec.Error `json:"Golang errors"`
|
||||
Issues []*gosec.Issue
|
||||
Stats *gosec.Metrics
|
||||
}
|
||||
|
||||
// CreateReport generates a report based for the supplied issues and metrics given
|
||||
// the specified format. The formats currently accepted are: json, yaml, csv, junit-xml, html, sonarqube, golint and text.
|
||||
func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []string, issues []*gosec.Issue, metrics *gosec.Metrics, errors map[string][]gosec.Error) error {
|
||||
data := &reportInfo{
|
||||
data := &formatter.ReportInfo{
|
||||
Errors: errors,
|
||||
Issues: issues,
|
||||
Stats: metrics,
|
||||
|
@ -116,7 +112,7 @@ func CreateReport(w io.Writer, format string, enableColor bool, rootPaths []stri
|
|||
return err
|
||||
}
|
||||
|
||||
func reportSonarqube(rootPaths []string, w io.Writer, data *reportInfo) error {
|
||||
func reportSonarqube(rootPaths []string, w io.Writer, data *formatter.ReportInfo) error {
|
||||
si, err := convertToSonarIssues(rootPaths, data)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -129,67 +125,7 @@ func reportSonarqube(rootPaths []string, w io.Writer, data *reportInfo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func convertToSarifReport(rootPaths []string, data *reportInfo) (*sarifReport, error) {
|
||||
sr := buildSarifReport()
|
||||
|
||||
type rule struct {
|
||||
index int
|
||||
rule *sarifRule
|
||||
}
|
||||
|
||||
rules := make([]*sarifRule, 0)
|
||||
rulesIndices := make(map[string]rule)
|
||||
lastRuleIndex := -1
|
||||
|
||||
results := []*sarifResult{}
|
||||
|
||||
for _, issue := range data.Issues {
|
||||
r, ok := rulesIndices[issue.RuleID]
|
||||
if !ok {
|
||||
lastRuleIndex++
|
||||
r = rule{index: lastRuleIndex, rule: buildSarifRule(issue)}
|
||||
rulesIndices[issue.RuleID] = r
|
||||
rules = append(rules, r.rule)
|
||||
}
|
||||
|
||||
location, err := buildSarifLocation(issue, rootPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := &sarifResult{
|
||||
RuleID: r.rule.ID,
|
||||
RuleIndex: r.index,
|
||||
Level: getSarifLevel(issue.Severity.String()),
|
||||
Message: &sarifMessage{
|
||||
Text: issue.What,
|
||||
},
|
||||
Locations: []*sarifLocation{location},
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
tool := &sarifTool{
|
||||
Driver: &sarifDriver{
|
||||
Name: "gosec",
|
||||
Version: "2.1.0",
|
||||
InformationURI: "https://github.com/securego/gosec/",
|
||||
Rules: rules,
|
||||
},
|
||||
}
|
||||
|
||||
run := &sarifRun{
|
||||
Tool: tool,
|
||||
Results: results,
|
||||
}
|
||||
|
||||
sr.Runs = append(sr.Runs, run)
|
||||
|
||||
return sr, nil
|
||||
}
|
||||
|
||||
func reportJSON(w io.Writer, data *reportInfo) error {
|
||||
func reportJSON(w io.Writer, data *formatter.ReportInfo) error {
|
||||
raw, err := json.MarshalIndent(data, "", "\t")
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -199,7 +135,7 @@ func reportJSON(w io.Writer, data *reportInfo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func reportYAML(w io.Writer, data *reportInfo) error {
|
||||
func reportYAML(w io.Writer, data *formatter.ReportInfo) error {
|
||||
raw, err := yaml.Marshal(data)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -208,7 +144,7 @@ func reportYAML(w io.Writer, data *reportInfo) error {
|
|||
return err
|
||||
}
|
||||
|
||||
func reportCSV(w io.Writer, data *reportInfo) error {
|
||||
func reportCSV(w io.Writer, data *formatter.ReportInfo) error {
|
||||
out := csv.NewWriter(w)
|
||||
defer out.Flush()
|
||||
for _, issue := range data.Issues {
|
||||
|
@ -228,7 +164,7 @@ func reportCSV(w io.Writer, data *reportInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func reportGolint(w io.Writer, data *reportInfo) error {
|
||||
func reportGolint(w io.Writer, data *formatter.ReportInfo) error {
|
||||
// Output Sample:
|
||||
// /tmp/main.go:11:14: [CWE-310] RSA keys should be at least 2048 bits (Rule:G403, Severity:MEDIUM, Confidence:HIGH)
|
||||
|
||||
|
@ -258,7 +194,7 @@ func reportGolint(w io.Writer, data *reportInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func reportJUnitXML(w io.Writer, data *reportInfo) error {
|
||||
func reportJUnitXML(w io.Writer, data *formatter.ReportInfo) error {
|
||||
junitXMLStruct := createJUnitXMLStruct(data)
|
||||
raw, err := xml.MarshalIndent(junitXMLStruct, "", "\t")
|
||||
if err != nil {
|
||||
|
@ -275,8 +211,8 @@ func reportJUnitXML(w io.Writer, data *reportInfo) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func reportSARIFTemplate(rootPaths []string, w io.Writer, data *reportInfo) error {
|
||||
sr, err := convertToSarifReport(rootPaths, data)
|
||||
func reportSARIFTemplate(rootPaths []string, w io.Writer, data *formatter.ReportInfo) error {
|
||||
sr, err := sarif.ConvertToSarifReport(rootPaths, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -289,7 +225,7 @@ func reportSARIFTemplate(rootPaths []string, w io.Writer, data *reportInfo) erro
|
|||
return err
|
||||
}
|
||||
|
||||
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *reportInfo) error {
|
||||
func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor bool, data *formatter.ReportInfo) error {
|
||||
t, e := plainTemplate.
|
||||
New("gosec").
|
||||
Funcs(plainTextFuncMap(enableColor)).
|
||||
|
@ -301,7 +237,7 @@ func reportFromPlaintextTemplate(w io.Writer, reportTemplate string, enableColor
|
|||
return t.Execute(w, data)
|
||||
}
|
||||
|
||||
func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *reportInfo) error {
|
||||
func reportFromHTMLTemplate(w io.Writer, reportTemplate string, data *formatter.ReportInfo) error {
|
||||
t, e := htmlTemplate.New("gosec").Parse(reportTemplate)
|
||||
if e != nil {
|
||||
return e
|
||||
|
|
|
@ -9,6 +9,7 @@ import (
|
|||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
"github.com/securego/gosec/v2"
|
||||
"github.com/securego/gosec/v2/formatter"
|
||||
"github.com/securego/gosec/v2/sonar"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
@ -34,10 +35,10 @@ func createIssue(ruleID string, cwe gosec.Cwe) gosec.Issue {
|
|||
}
|
||||
}
|
||||
|
||||
func createReportInfo(rule string, cwe gosec.Cwe) reportInfo {
|
||||
func createReportInfo(rule string, cwe gosec.Cwe) formatter.ReportInfo {
|
||||
issue := createIssue(rule, cwe)
|
||||
metrics := gosec.Metrics{}
|
||||
return reportInfo{
|
||||
return formatter.ReportInfo{
|
||||
Errors: map[string][]gosec.Error{},
|
||||
Issues: []*gosec.Issue{
|
||||
&issue,
|
||||
|
@ -58,7 +59,7 @@ var _ = Describe("Formatter", func() {
|
|||
})
|
||||
Context("when converting to Sonarqube issues", func() {
|
||||
It("it should parse the report info", func() {
|
||||
data := &reportInfo{
|
||||
data := &formatter.ReportInfo{
|
||||
Errors: map[string][]gosec.Error{},
|
||||
Issues: []*gosec.Issue{
|
||||
{
|
||||
|
@ -106,7 +107,7 @@ var _ = Describe("Formatter", func() {
|
|||
})
|
||||
|
||||
It("it should parse the report info with files in subfolders", func() {
|
||||
data := &reportInfo{
|
||||
data := &formatter.ReportInfo{
|
||||
Errors: map[string][]gosec.Error{},
|
||||
Issues: []*gosec.Issue{
|
||||
{
|
||||
|
@ -153,7 +154,7 @@ var _ = Describe("Formatter", func() {
|
|||
Expect(*issues).To(Equal(*want))
|
||||
})
|
||||
It("it should not parse the report info for files from other projects", func() {
|
||||
data := &reportInfo{
|
||||
data := &formatter.ReportInfo{
|
||||
Errors: map[string][]gosec.Error{},
|
||||
Issues: []*gosec.Issue{
|
||||
{
|
||||
|
@ -185,7 +186,7 @@ var _ = Describe("Formatter", func() {
|
|||
})
|
||||
|
||||
It("it should parse the report info for multiple projects projects", func() {
|
||||
data := &reportInfo{
|
||||
data := &formatter.ReportInfo{
|
||||
Errors: map[string][]gosec.Error{},
|
||||
Issues: []*gosec.Issue{
|
||||
{
|
||||
|
@ -261,7 +262,7 @@ var _ = Describe("Formatter", func() {
|
|||
It("preserves order of issues", func() {
|
||||
issues := []*gosec.Issue{createIssueWithFileWhat("i1", "1"), createIssueWithFileWhat("i2", "2"), createIssueWithFileWhat("i3", "1")}
|
||||
|
||||
junitReport := createJUnitXMLStruct(&reportInfo{Issues: issues})
|
||||
junitReport := createJUnitXMLStruct(&formatter.ReportInfo{Issues: issues})
|
||||
|
||||
testSuite := junitReport.Testsuites[0]
|
||||
|
||||
|
@ -454,11 +455,23 @@ var _ = Describe("Formatter", func() {
|
|||
|
||||
result := stripString(buf.String())
|
||||
|
||||
pattern := "rules\":[{\"id\":\"%s(CWE-%s)\""
|
||||
expect := fmt.Sprintf(pattern, rule, cwe.ID)
|
||||
ruleIDPattern := "\"id\":\"%s\""
|
||||
expectedRule := fmt.Sprintf(ruleIDPattern, rule)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
Expect(result).To(ContainSubstring(expect))
|
||||
Expect(result).To(ContainSubstring(expectedRule))
|
||||
|
||||
cweURIPattern := "\"helpUri\":\"https://cwe.mitre.org/data/definitions/%s.html\""
|
||||
expectedCweURI := fmt.Sprintf(cweURIPattern, cwe.ID)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
Expect(result).To(ContainSubstring(expectedCweURI))
|
||||
|
||||
cweIDPattern := "\"id\":\"%s\""
|
||||
expectedCweID := fmt.Sprintf(cweIDPattern, cwe.ID)
|
||||
Expect(err).ShouldNot(HaveOccurred())
|
||||
|
||||
Expect(result).To(ContainSubstring(expectedCweID))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strconv"
|
||||
|
||||
"github.com/securego/gosec/v2"
|
||||
"github.com/securego/gosec/v2/formatter"
|
||||
)
|
||||
|
||||
type junitXMLReport struct {
|
||||
|
@ -40,7 +41,7 @@ func generatePlaintext(issue *gosec.Issue) string {
|
|||
", CWE: " + issue.Cwe.ID + ")\n" + "> " + htmlLib.EscapeString(issue.Code)
|
||||
}
|
||||
|
||||
func createJUnitXMLStruct(data *reportInfo) junitXMLReport {
|
||||
func createJUnitXMLStruct(data *formatter.ReportInfo) junitXMLReport {
|
||||
var xmlReport junitXMLReport
|
||||
testsuites := map[string]int{}
|
||||
|
||||
|
|
|
@ -1,184 +0,0 @@
|
|||
package output
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/securego/gosec/v2"
|
||||
)
|
||||
|
||||
type sarifLevel string
|
||||
|
||||
const (
|
||||
sarifNone = sarifLevel("none")
|
||||
sarifNote = sarifLevel("note")
|
||||
sarifWarning = sarifLevel("warning")
|
||||
sarifError = sarifLevel("error")
|
||||
)
|
||||
|
||||
type sarifProperties struct {
|
||||
Tags []string `json:"tags"`
|
||||
}
|
||||
|
||||
type sarifRule struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
ShortDescription *sarifMessage `json:"shortDescription"`
|
||||
FullDescription *sarifMessage `json:"fullDescription"`
|
||||
Help *sarifMessage `json:"help"`
|
||||
Properties *sarifProperties `json:"properties"`
|
||||
DefaultConfiguration *sarifConfiguration `json:"defaultConfiguration"`
|
||||
}
|
||||
|
||||
type sarifConfiguration struct {
|
||||
Level sarifLevel `json:"level"`
|
||||
}
|
||||
|
||||
type sarifArtifactLocation struct {
|
||||
URI string `json:"uri"`
|
||||
}
|
||||
|
||||
type sarifRegion struct {
|
||||
StartLine uint64 `json:"startLine"`
|
||||
EndLine uint64 `json:"endLine"`
|
||||
StartColumn uint64 `json:"startColumn"`
|
||||
EndColumn uint64 `json:"endColumn"`
|
||||
}
|
||||
|
||||
type sarifPhysicalLocation struct {
|
||||
ArtifactLocation *sarifArtifactLocation `json:"artifactLocation"`
|
||||
Region *sarifRegion `json:"region"`
|
||||
}
|
||||
|
||||
type sarifLocation struct {
|
||||
PhysicalLocation *sarifPhysicalLocation `json:"physicalLocation"`
|
||||
}
|
||||
|
||||
type sarifMessage struct {
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type sarifResult struct {
|
||||
RuleID string `json:"ruleId"`
|
||||
RuleIndex int `json:"ruleIndex"`
|
||||
Level sarifLevel `json:"level"`
|
||||
Message *sarifMessage `json:"message"`
|
||||
Locations []*sarifLocation `json:"locations"`
|
||||
}
|
||||
|
||||
type sarifDriver struct {
|
||||
Name string `json:"name"`
|
||||
Version string `json:"version"`
|
||||
InformationURI string `json:"informationUri"`
|
||||
Rules []*sarifRule `json:"rules,omitempty"`
|
||||
}
|
||||
|
||||
type sarifTool struct {
|
||||
Driver *sarifDriver `json:"driver"`
|
||||
}
|
||||
|
||||
type sarifRun struct {
|
||||
Tool *sarifTool `json:"tool"`
|
||||
Results []*sarifResult `json:"results"`
|
||||
}
|
||||
|
||||
type sarifReport struct {
|
||||
Schema string `json:"$schema"`
|
||||
Version string `json:"version"`
|
||||
Runs []*sarifRun `json:"runs"`
|
||||
}
|
||||
|
||||
// buildSarifReport return SARIF report struct
|
||||
func buildSarifReport() *sarifReport {
|
||||
return &sarifReport{
|
||||
Version: "2.1.0",
|
||||
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
||||
Runs: []*sarifRun{},
|
||||
}
|
||||
}
|
||||
|
||||
// buildSarifRule return SARIF rule field struct
|
||||
func buildSarifRule(issue *gosec.Issue) *sarifRule {
|
||||
return &sarifRule{
|
||||
ID: fmt.Sprintf("%s (CWE-%s)", issue.RuleID, issue.Cwe.ID),
|
||||
Name: issue.What,
|
||||
ShortDescription: &sarifMessage{
|
||||
Text: issue.What,
|
||||
},
|
||||
FullDescription: &sarifMessage{
|
||||
Text: issue.What,
|
||||
},
|
||||
Help: &sarifMessage{
|
||||
Text: fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\nCWE: %s", issue.What, issue.Severity.String(), issue.Confidence.String(), issue.Cwe.URL),
|
||||
},
|
||||
Properties: &sarifProperties{
|
||||
Tags: []string{fmt.Sprintf("CWE-%s", issue.Cwe.ID), issue.Severity.String()},
|
||||
},
|
||||
DefaultConfiguration: &sarifConfiguration{
|
||||
Level: getSarifLevel(issue.Severity.String()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// buildSarifLocation return SARIF location struct
|
||||
func buildSarifLocation(issue *gosec.Issue, rootPaths []string) (*sarifLocation, error) {
|
||||
var filePath string
|
||||
|
||||
lines := strings.Split(issue.Line, "-")
|
||||
startLine, err := strconv.ParseUint(lines[0], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endLine := startLine
|
||||
if len(lines) > 1 {
|
||||
endLine, err = strconv.ParseUint(lines[1], 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
col, err := strconv.ParseUint(issue.Col, 10, 64)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, rootPath := range rootPaths {
|
||||
if strings.HasPrefix(issue.File, rootPath) {
|
||||
filePath = strings.Replace(issue.File, rootPath+"/", "", 1)
|
||||
}
|
||||
}
|
||||
|
||||
location := &sarifLocation{
|
||||
PhysicalLocation: &sarifPhysicalLocation{
|
||||
ArtifactLocation: &sarifArtifactLocation{
|
||||
URI: filePath,
|
||||
},
|
||||
Region: &sarifRegion{
|
||||
StartLine: startLine,
|
||||
EndLine: endLine,
|
||||
StartColumn: col,
|
||||
EndColumn: col,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
return location, nil
|
||||
}
|
||||
|
||||
// From https://docs.oasis-open.org/sarif/sarif/v2.0/csprd02/sarif-v2.0-csprd02.html#_Toc10127839
|
||||
// * "warning": The rule specified by ruleId was evaluated and a problem was found.
|
||||
// * "error": The rule specified by ruleId was evaluated and a serious problem was found.
|
||||
// * "note": The rule specified by ruleId was evaluated and a minor problem or an opportunity to improve the code was found.
|
||||
func getSarifLevel(s string) sarifLevel {
|
||||
switch s {
|
||||
case "LOW":
|
||||
return sarifWarning
|
||||
case "MEDIUM":
|
||||
return sarifError
|
||||
case "HIGH":
|
||||
return sarifError
|
||||
default:
|
||||
return sarifNote
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package output
|
|||
|
||||
import (
|
||||
"github.com/securego/gosec/v2"
|
||||
"github.com/securego/gosec/v2/formatter"
|
||||
"github.com/securego/gosec/v2/sonar"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
@ -12,7 +13,7 @@ const (
|
|||
SonarqubeEffortMinutes = 5
|
||||
)
|
||||
|
||||
func convertToSonarIssues(rootPaths []string, data *reportInfo) (*sonar.Report, error) {
|
||||
func convertToSonarIssues(rootPaths []string, data *formatter.ReportInfo) (*sonar.Report, error) {
|
||||
si := &sonar.Report{Issues: []*sonar.Issue{}}
|
||||
for _, issue := range data.Issues {
|
||||
sonarFilePath := parseFilePath(issue, rootPaths)
|
||||
|
|
297
sarif/formatter.go
Normal file
297
sarif/formatter.go
Normal file
|
@ -0,0 +1,297 @@
|
|||
package sarif
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/google/uuid"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/securego/gosec/v2"
|
||||
"github.com/securego/gosec/v2/cwe"
|
||||
"github.com/securego/gosec/v2/formatter"
|
||||
)
|
||||
|
||||
type sarifLevel string
|
||||
|
||||
const (
|
||||
sarifNone = sarifLevel("none")
|
||||
sarifNote = sarifLevel("note")
|
||||
sarifWarning = sarifLevel("warning")
|
||||
sarifError = sarifLevel("error")
|
||||
cweAcronym = "CWE"
|
||||
)
|
||||
|
||||
//ConvertToSarifReport Convert a gosec report to a Sarif Report
|
||||
func ConvertToSarifReport(rootPaths []string, data *formatter.ReportInfo) (*Report, error) {
|
||||
|
||||
type rule struct {
|
||||
index int
|
||||
rule *ReportingDescriptor
|
||||
}
|
||||
|
||||
rules := make([]*ReportingDescriptor, 0)
|
||||
rulesIndices := make(map[string]rule)
|
||||
lastRuleIndex := -1
|
||||
|
||||
results := []*Result{}
|
||||
taxa := make([]*ReportingDescriptor, 0)
|
||||
weaknesses := make(map[string]cwe.Weakness)
|
||||
|
||||
for _, issue := range data.Issues {
|
||||
_, ok := weaknesses[issue.Cwe.ID]
|
||||
if !ok {
|
||||
weakness := cwe.Get(issue.Cwe.ID)
|
||||
weaknesses[issue.Cwe.ID] = weakness
|
||||
taxon := parseSarifTaxon(weakness)
|
||||
taxa = append(taxa, taxon)
|
||||
}
|
||||
|
||||
r, ok := rulesIndices[issue.RuleID]
|
||||
if !ok {
|
||||
lastRuleIndex++
|
||||
r = rule{index: lastRuleIndex, rule: parseSarifRule(issue)}
|
||||
rulesIndices[issue.RuleID] = r
|
||||
rules = append(rules, r.rule)
|
||||
}
|
||||
|
||||
location, err := parseSarifLocation(issue, rootPaths)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := buildSarifResult(r.rule.ID, r.index, issue, []*Location{location})
|
||||
|
||||
results = append(results, result)
|
||||
}
|
||||
|
||||
tool := buildSarifTool(buildSarifDriver(rules))
|
||||
|
||||
run := buildSarifRun(results, buildSarifTaxonomies(taxa), tool)
|
||||
|
||||
return buildSarifReport(run), nil
|
||||
}
|
||||
|
||||
func buildSarifResult(ruleID string, index int, issue *gosec.Issue, locations []*Location) *Result {
|
||||
return &Result{
|
||||
RuleID: ruleID,
|
||||
RuleIndex: index,
|
||||
Level: getSarifLevel(issue.Severity.String()),
|
||||
Message: &Message{
|
||||
Text: issue.What,
|
||||
},
|
||||
Locations: locations,
|
||||
}
|
||||
}
|
||||
|
||||
// buildSarifReport return SARIF report struct
|
||||
func buildSarifReport(run *Run) *Report {
|
||||
return &Report{
|
||||
Version: "2.1.0",
|
||||
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
|
||||
Runs: []*Run{run},
|
||||
}
|
||||
}
|
||||
|
||||
// parseSarifRule return SARIF rule field struct
|
||||
func parseSarifRule(issue *gosec.Issue) *ReportingDescriptor {
|
||||
return &ReportingDescriptor{
|
||||
ID: issue.RuleID,
|
||||
Name: issue.What,
|
||||
ShortDescription: &MultiformatMessageString{
|
||||
Text: issue.What,
|
||||
},
|
||||
FullDescription: &MultiformatMessageString{
|
||||
Text: issue.What,
|
||||
},
|
||||
Help: &MultiformatMessageString{
|
||||
Text: fmt.Sprintf("%s\nSeverity: %s\nConfidence: %s\n", issue.What, issue.Severity.String(), issue.Confidence.String()),
|
||||
},
|
||||
Properties: &PropertyBag{
|
||||
Tags: []string{"security", issue.Severity.String()},
|
||||
AdditionalProperties: map[string]interface{}{
|
||||
"precision": strings.ToLower(issue.Confidence.String()),
|
||||
},
|
||||
},
|
||||
DefaultConfiguration: &ReportingConfiguration{
|
||||
Level: getSarifLevel(issue.Severity.String()),
|
||||
},
|
||||
Relationships: []*ReportingDescriptorRelationship{
|
||||
buildSarifReportingDescriptorRelationship(issue.Cwe.ID),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildSarifReportingDescriptorRelationship(weaknessID string) *ReportingDescriptorRelationship {
|
||||
return &ReportingDescriptorRelationship{
|
||||
Target: &ReportingDescriptorReference{
|
||||
ID: weaknessID,
|
||||
GUID: uuid3(cweAcronym + weaknessID),
|
||||
ToolComponent: &ToolComponentReference{
|
||||
Name: cweAcronym,
|
||||
},
|
||||
},
|
||||
Kinds: []string{"superset"},
|
||||
}
|
||||
}
|
||||
|
||||
func buildSarifTool(driver *ToolComponent) *Tool {
|
||||
return &Tool{
|
||||
Driver: driver,
|
||||
}
|
||||
}
|
||||
|
||||
func buildSarifTaxonomies(taxa []*ReportingDescriptor) []*ToolComponent {
|
||||
return []*ToolComponent{
|
||||
buildCWETaxonomy("4.4", taxa),
|
||||
}
|
||||
}
|
||||
|
||||
func buildCWETaxonomy(version string, taxa []*ReportingDescriptor) *ToolComponent {
|
||||
return &ToolComponent{
|
||||
Name: cweAcronym,
|
||||
Version: version,
|
||||
ReleaseDateUtc: "2021-03-15",
|
||||
InformationURI: fmt.Sprintf("https://cwe.mitre.org/data/published/cwe_v%s.pdf/", version),
|
||||
DownloadURI: fmt.Sprintf("https://cwe.mitre.org/data/xml/cwec_v%s.xml.zip", version),
|
||||
Organization: "MITRE",
|
||||
ShortDescription: &MultiformatMessageString{
|
||||
Text: "The MITRE Common Weakness Enumeration",
|
||||
},
|
||||
GUID: uuid3(cweAcronym),
|
||||
IsComprehensive: true,
|
||||
MinimumRequiredLocalizedDataSemanticVersion: version,
|
||||
Taxa: taxa,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSarifTaxon(weakness cwe.Weakness) *ReportingDescriptor {
|
||||
return &ReportingDescriptor{
|
||||
ID: weakness.ID,
|
||||
Name: weakness.Name,
|
||||
GUID: uuid3(cweAcronym + weakness.ID),
|
||||
HelpURI: weakness.URL(),
|
||||
ShortDescription: &MultiformatMessageString{
|
||||
Text: weakness.Description,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildSarifDriver(rules []*ReportingDescriptor) *ToolComponent {
|
||||
buildInfo, ok := debug.ReadBuildInfo()
|
||||
var gosecVersion string
|
||||
if ok {
|
||||
gosecVersion = buildInfo.Main.Version[1:]
|
||||
} else {
|
||||
gosecVersion = "devel"
|
||||
}
|
||||
return &ToolComponent{
|
||||
Name: "gosec",
|
||||
Version: gosecVersion,
|
||||
SupportedTaxonomies: []*ToolComponentReference{
|
||||
{Name: cweAcronym, GUID: uuid3(cweAcronym)},
|
||||
},
|
||||
InformationURI: "https://github.com/securego/gosec/",
|
||||
Rules: rules,
|
||||
}
|
||||
}
|
||||
|
||||
func uuid3(value string) string {
|
||||
return uuid.NewMD5(uuid.Nil, []byte(value)).String()
|
||||
}
|
||||
|
||||
func buildSarifRun(results []*Result, taxonomies []*ToolComponent, tool *Tool) *Run {
|
||||
return &Run{
|
||||
Results: results,
|
||||
Taxonomies: taxonomies,
|
||||
Tool: tool,
|
||||
}
|
||||
}
|
||||
|
||||
// parseSarifLocation return SARIF location struct
|
||||
func parseSarifLocation(issue *gosec.Issue, rootPaths []string) (*Location, error) {
|
||||
region, err := parseSarifRegion(issue)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
artifactLocation := parseSarifArtifactLocation(issue, rootPaths)
|
||||
return buildSarifLocation(buildSarifPhysicalLocation(artifactLocation, region)), nil
|
||||
}
|
||||
|
||||
func buildSarifLocation(physicalLocation *PhysicalLocation) *Location {
|
||||
return &Location{
|
||||
PhysicalLocation: physicalLocation,
|
||||
}
|
||||
}
|
||||
|
||||
func buildSarifPhysicalLocation(artifactLocation *ArtifactLocation, region *Region) *PhysicalLocation {
|
||||
return &PhysicalLocation{
|
||||
ArtifactLocation: artifactLocation,
|
||||
Region: region,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSarifArtifactLocation(issue *gosec.Issue, rootPaths []string) *ArtifactLocation {
|
||||
var filePath string
|
||||
for _, rootPath := range rootPaths {
|
||||
if strings.HasPrefix(issue.File, rootPath) {
|
||||
filePath = strings.Replace(issue.File, rootPath+"/", "", 1)
|
||||
}
|
||||
}
|
||||
return buildSarifArtifactLocation(filePath)
|
||||
}
|
||||
|
||||
func buildSarifArtifactLocation(uri string) *ArtifactLocation {
|
||||
return &ArtifactLocation{
|
||||
URI: uri,
|
||||
}
|
||||
}
|
||||
|
||||
func parseSarifRegion(issue *gosec.Issue) (*Region, error) {
|
||||
lines := strings.Split(issue.Line, "-")
|
||||
startLine, err := strconv.Atoi(lines[0])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endLine := startLine
|
||||
if len(lines) > 1 {
|
||||
endLine, err = strconv.Atoi(lines[1])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
col, err := strconv.Atoi(issue.Col)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buildSarifRegion(startLine, endLine, col), nil
|
||||
}
|
||||
|
||||
func buildSarifRegion(startLine int, endLine int, col int) *Region {
|
||||
return &Region{
|
||||
StartLine: startLine,
|
||||
EndLine: endLine,
|
||||
StartColumn: col,
|
||||
EndColumn: col,
|
||||
SourceLanguage: "go",
|
||||
}
|
||||
}
|
||||
|
||||
// From https://docs.oasis-open.org/sarif/sarif/v2.0/csprd02/sarif-v2.0-csprd02.html#_Toc10127839
|
||||
// * "warning": The rule specified by ruleId was evaluated and a problem was found.
|
||||
// * "error": The rule specified by ruleId was evaluated and a serious problem was found.
|
||||
// * "note": The rule specified by ruleId was evaluated and a minor problem or an opportunity to improve the code was found.
|
||||
// * "none": The concept of “severity” does not apply to this result because the kind property (§3.27.9) has a value other than "fail".
|
||||
func getSarifLevel(s string) sarifLevel {
|
||||
switch s {
|
||||
case "LOW":
|
||||
return sarifWarning
|
||||
case "MEDIUM":
|
||||
return sarifError
|
||||
case "HIGH":
|
||||
return sarifError
|
||||
default:
|
||||
return sarifNote
|
||||
}
|
||||
}
|
1516
sarif/types.go
Normal file
1516
sarif/types.go
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Reference in a new issue