diff --git a/.github/README.md b/.github/README.md index 1c144ed5..62fe2d8c 100644 --- a/.github/README.md +++ b/.github/README.md @@ -131,6 +131,7 @@ Files supported by the `out` parameter are listed below: | | `html` | `.html` | `v1.0.6` and above | | | `sqlite` | `.sqlite` | `v1.0.13` and above| | | `csv` | `.csv` | `v1.0.13` and above| +| | `sarif`| `.sarif` | | | SBOM | `spdx` | `.spdx` `.spdx.json` `.spdx.xml` | `v1.0.8` and above | | | `cdx` | `.cdx.json` `.cdx.xml` | `v1.0.11`and above | | | `swid` | `.swid.json` `.swid.xml` | `v1.0.11`and above | diff --git a/README.md b/README.md index 9daa81df..1dffde07 100644 --- a/README.md +++ b/README.md @@ -126,6 +126,7 @@ v3.0.2开始,OpenSCA-cli可以通过proj参数向OpenSCA SaaS同步检出结 | | `html` | `.html` | | | `sqlite` | `.sqlite` | | | `csv` | `.csv` | +| | `sarif` | `.sarif` | | SBOM清单 | `spdx` | `.spdx` `.spdx.json` `.spdx.xml` | | | `cdx` | `.cdx.json` `.cdx.xml` | | | `swid` | `.swid.json` `.swid.xml` | diff --git a/cmd/detail/detail.go b/cmd/detail/detail.go index 91045cd9..ef9a0485 100644 --- a/cmd/detail/detail.go +++ b/cmd/detail/detail.go @@ -156,6 +156,20 @@ type Vuln struct { ExploitLevelId int `json:"exploit_level_id" gorm:"column:exploit_level_id"` } +func (v *Vuln) SecurityLevel() string { + switch v.SecurityLevelId { + case 1: + return "Critical" + case 2: + return "High" + case 3: + return "Medium" + case 4: + return "Low" + } + return "Unknown" +} + func vulnLanguageKey(language model.Language) []string { switch language { case model.Lan_Java: diff --git a/cmd/format/csv.go b/cmd/format/csv.go index 43e8f74e..840fb4d0 100644 --- a/cmd/format/csv.go +++ b/cmd/format/csv.go @@ -25,8 +25,9 @@ func Csv(report Report, out string) { return true }) - outWrite(out, func(w io.Writer) { - w.Write([]byte(table)) + outWrite(out, func(w io.Writer) error { + _, err := w.Write([]byte(table)) + return err }) } diff --git a/cmd/format/cyclonedx.go b/cmd/format/cyclonedx.go index b783ef98..6c5dc779 100644 --- a/cmd/format/cyclonedx.go +++ b/cmd/format/cyclonedx.go @@ -58,14 +58,14 @@ func cyclonedxbom(dep *detail.DepDetailGraph) *cyclonedx.BOM { func CycloneDXJson(report Report, out string) { bom := cyclonedxbom(report.DepDetailGraph) - outWrite(out, func(w io.Writer) { - cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatJSON).SetPretty(true).Encode(bom) + outWrite(out, func(w io.Writer) error { + return cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatJSON).SetPretty(true).Encode(bom) }) } func CycloneDXXml(report Report, out string) { bom := cyclonedxbom(report.DepDetailGraph) - outWrite(out, func(w io.Writer) { - cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatXML).SetPretty(true).Encode(bom) + outWrite(out, func(w io.Writer) error { + return cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatXML).SetPretty(true).Encode(bom) }) } diff --git a/cmd/format/dsdx.go b/cmd/format/dsdx.go index 700334bc..63bae61f 100644 --- a/cmd/format/dsdx.go +++ b/cmd/format/dsdx.go @@ -6,28 +6,24 @@ import ( "io" "github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail" - "github.com/xmirrorsecurity/opensca-cli/v3/opensca/logs" "github.com/xmirrorsecurity/opensca-cli/v3/opensca/model" ) func Dsdx(report Report, out string) { - outWrite(out, func(w io.Writer) { - err := dsdxDoc(report).WriteDsdx(w) - if err != nil { - logs.Warn(err) - } + outWrite(out, func(w io.Writer) error { + return dsdxDoc(report).WriteDsdx(w) }) } func DsdxJson(report Report, out string) { - outWrite(out, func(w io.Writer) { - json.NewEncoder(w).Encode(dsdxDoc(report)) + outWrite(out, func(w io.Writer) error { + return json.NewEncoder(w).Encode(dsdxDoc(report)) }) } func DsdxXml(report Report, out string) { - outWrite(out, func(w io.Writer) { - xml.NewEncoder(w).Encode(dsdxDoc(report)) + outWrite(out, func(w io.Writer) error { + return xml.NewEncoder(w).Encode(dsdxDoc(report)) }) } diff --git a/cmd/format/html.go b/cmd/format/html.go index 99bbad36..18844d4b 100644 --- a/cmd/format/html.go +++ b/cmd/format/html.go @@ -86,8 +86,9 @@ func Html(report Report, out string) { }); err != nil { logs.Warn(err) } else { - outWrite(out, func(w io.Writer) { - w.Write(bytes.Replace(index, []byte(`"此处填充json数据"`), data, 1)) + outWrite(out, func(w io.Writer) error { + _, err := w.Write(bytes.Replace(index, []byte(`"此处填充json数据"`), data, 1)) + return err }) return } diff --git a/cmd/format/json.go b/cmd/format/json.go index 7144b34d..7965319b 100644 --- a/cmd/format/json.go +++ b/cmd/format/json.go @@ -6,9 +6,9 @@ import ( ) func Json(report Report, out string) { - outWrite(out, func(w io.Writer) { + outWrite(out, func(w io.Writer) error { encoder := json.NewEncoder(w) encoder.SetIndent("", " ") - encoder.Encode(report) + return encoder.Encode(report) }) } diff --git a/cmd/format/sarif.go b/cmd/format/sarif.go new file mode 100644 index 00000000..b0f6c31d --- /dev/null +++ b/cmd/format/sarif.go @@ -0,0 +1,195 @@ +package format + +import ( + "encoding/json" + "fmt" + "io" + "regexp" + "strings" + + "github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail" +) + +type sarifReport struct { + Version string `json:"version"` + Schema string `json:"$schema"` + Runs []sarifRun `json:"runs"` +} + +type sarifRun struct { + Tool struct { + Driver struct { + Name string `json:"name"` + Version string `json:"version"` + InformationUri string `json:"informationUri"` + Rules []sarifRule `json:"rules"` + } `json:"driver"` + } `json:"tool"` + Results []sarifResult `json:"results"` +} + +type sarifRule struct { + Id string `json:"id"` + Name string `json:"name"` + ShortDescription sarifRuleShortDescription `json:"shortDescription"` + FullDescription sarifRuleFullDescription `json:"fullDescription"` + Help sarifRuleHelp `json:"help"` + Properties sarifRuleProperties `json:"properties"` +} + +type sarifRuleShortDescription struct { + Text string `json:"text"` +} + +type sarifRuleFullDescription struct { + Text string `json:"text"` +} + +type sarifRuleHelp struct { + Text string `json:"text"` + Markdown string `json:"markdown"` +} + +type sarifRuleProperties struct { + Tags []string `json:"tags"` +} + +type sarifResult struct { + RuleId string `json:"ruleId"` + Level string `json:"level"` + Message struct { + Text string `json:"text"` + } `json:"message"` + Locations []sarifLocation `json:"locations"` +} + +type sarifLocation struct { + PhysicalLocation struct { + ArtifactLocation struct { + Uri string `json:"uri"` + Index int `json:"index,omitempty"` + } `json:"artifactLocation"` + Region struct { + StartColumn int `json:"startColumn"` + EndColumn int `json:"endColumn"` + StartLine int `json:"startLine"` + EndLine int `json:"endLine"` + } `json:"region"` + } `json:"physicalLocation"` +} + +func Sarif(report Report, out string) { + + s := sarifReport{ + Version: "2.1.0", + Schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json", + } + + run := sarifRun{} + run.Tool.Driver.Name = "opensca-cli" + run.Tool.Driver.Version = strings.TrimLeft(report.TaskInfo.ToolVersion, "vV") + run.Tool.Driver.InformationUri = "https://opensca.xmirror.cn" + + vulnInfos := map[string]*detail.VulnInfo{} + + report.ForEach(func(n *detail.DepDetailGraph) bool { + for _, vuln := range n.Vulnerabilities { + + if vuln.Id == "" { + continue + } + + vulnInfos[vuln.Id] = &detail.VulnInfo{Vuln: vuln, Language: n.Language} + + result := sarifResult{ + RuleId: vuln.Id, + Level: "warning", + } + result.Message.Text = fmt.Sprintf("引入的组件 %s 中存在 %s", n.Dep.Key()[:strings.LastIndex(n.Dep.Key(), ":")], vuln.Name) + for i, path := range n.Paths { + if truncIndex := strings.Index(path, "["); truncIndex > 0 { + path = strings.Trim(path[:truncIndex], `\/`) + } + location := sarifLocation{} + location.PhysicalLocation.ArtifactLocation.Uri = path + location.PhysicalLocation.ArtifactLocation.Index = i + location.PhysicalLocation.Region.StartColumn = 1 + location.PhysicalLocation.Region.EndColumn = 1 + location.PhysicalLocation.Region.StartLine = 1 + location.PhysicalLocation.Region.EndLine = 1 + result.Locations = append(result.Locations, location) + } + + run.Results = append(run.Results, result) + } + return true + }) + + for _, vuln := range vulnInfos { + run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, sarifRule{ + Id: vuln.Id, + Name: vuln.Name, + ShortDescription: sarifRuleShortDescription{Text: vuln.Name}, + FullDescription: sarifRuleFullDescription{Text: vuln.Description}, + Help: sarifRuleHelp{Markdown: formatDesc(vuln)}, + Properties: sarifRuleProperties{Tags: formatTags(vuln)}, + }) + } + + s.Runs = []sarifRun{run} + outWrite(out, func(w io.Writer) error { + return json.NewEncoder(w).Encode(s) + }) +} + +func formatDesc(v *detail.VulnInfo) string { + table := []struct { + fmt string + val string + }{ + {"| id | %s |", v.Id}, + {"| --- | --- |", ""}, + {"| cve | %s |", v.Cve}, + {"| cnnvd | %s |", v.Cnnvd}, + {"| cnvd | %s |", v.Cnvd}, + {"| cwe | %s |", v.Cwe}, + {"| level | %s |", v.SecurityLevel()}, + {"| desc | %s |", sanitizeString(v.Description)}, + {"| suggestion | %s |", sanitizeString(v.Suggestion)}, + } + var lines []string + for _, line := range table { + if strings.Contains(line.fmt, "%s") && line.val == "" { + continue + } + if line.val == "" { + lines = append(lines, line.fmt) + } else { + lines = append(lines, fmt.Sprintf(line.fmt, line.val)) + } + } + + return strings.Join(lines, "\n") +} + +func sanitizeString(s string) string { + re := regexp.MustCompile("<[^>]*>") + s = re.ReplaceAllString(s, "") + + s = strings.ReplaceAll(s, "\r", "") + s = strings.ReplaceAll(s, "\n", "") + + return s +} + +func formatTags(v *detail.VulnInfo) []string { + tags := []string{"security", "Use-Vulnerable-and-Outdated-Components", v.Cve, v.Cwe, v.AttackType, v.Language} + for i := 0; i < len(tags); { + if tags[i] == "" { + tags = append(tags[:i], tags[i+1:]...) + } else { + i++ + } + } + return tags +} diff --git a/cmd/format/save.go b/cmd/format/save.go index 91546f70..e9b8361f 100644 --- a/cmd/format/save.go +++ b/cmd/format/save.go @@ -71,13 +71,15 @@ func Save(report Report, output string) { Csv(report, out) case ".sqlite", ".db": Sqlite(report, out) + case ".sarif": + Sarif(report, out) default: Json(report, out) } } } -func outWrite(out string, do func(io.Writer)) { +func outWrite(out string, do func(io.Writer) error) { if out == "" { do(os.Stdout) @@ -95,7 +97,9 @@ func outWrite(out string, do func(io.Writer)) { logs.Warn(err) } else { defer w.Close() - do(w) + if err = do(w); err != nil { + logs.Warn(err) + } } } diff --git a/cmd/format/spdx.go b/cmd/format/spdx.go index cd8a457f..313983f7 100644 --- a/cmd/format/spdx.go +++ b/cmd/format/spdx.go @@ -6,28 +6,24 @@ import ( "io" "github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail" - "github.com/xmirrorsecurity/opensca-cli/v3/opensca/logs" "github.com/xmirrorsecurity/opensca-cli/v3/opensca/model" ) func Spdx(report Report, out string) { - outWrite(out, func(w io.Writer) { - err := spdxDoc(report).WriteSpdx(w) - if err != nil { - logs.Warn(err) - } + outWrite(out, func(w io.Writer) error { + return spdxDoc(report).WriteSpdx(w) }) } func SpdxJson(report Report, out string) { - outWrite(out, func(w io.Writer) { - json.NewEncoder(w).Encode(spdxDoc(report)) + outWrite(out, func(w io.Writer) error { + return json.NewEncoder(w).Encode(spdxDoc(report)) }) } func SpdxXml(report Report, out string) { - outWrite(out, func(w io.Writer) { - xml.NewEncoder(w).Encode(spdxDoc(report)) + outWrite(out, func(w io.Writer) error { + return xml.NewEncoder(w).Encode(spdxDoc(report)) }) } diff --git a/cmd/format/swid.go b/cmd/format/swid.go index 78c61df8..fb954931 100644 --- a/cmd/format/swid.go +++ b/cmd/format/swid.go @@ -4,6 +4,7 @@ import ( "archive/zip" "encoding/json" "encoding/xml" + "errors" "io" "path/filepath" "strings" @@ -14,12 +15,14 @@ import ( "github.com/veraison/swid" ) -func swidZip(out string, report Report, writeFunc func(tag *swid.SoftwareIdentity, w io.Writer)) { - outWrite(out+".zip", func(writer io.Writer) { +func swidZip(out string, report Report, writeFunc func(tag *swid.SoftwareIdentity, w io.Writer) error) { + outWrite(out+".zip", func(writer io.Writer) error { zf := zip.NewWriter(writer) defer zf.Close() + var werr error + report.DepDetailGraph.ForEach(func(n *detail.DepDetailGraph) bool { if n.Name == "" { @@ -63,21 +66,23 @@ func swidZip(out string, report Report, writeFunc func(tag *swid.SoftwareIdentit return true } - writeFunc(tag, w) + werr = errors.Join(werr, writeFunc(tag, w)) + return true }) + return werr }) } func SwidJson(report Report, out string) { - swidZip(out, report, func(tag *swid.SoftwareIdentity, w io.Writer) { - json.NewEncoder(w).Encode(tag) + swidZip(out, report, func(tag *swid.SoftwareIdentity, w io.Writer) error { + return json.NewEncoder(w).Encode(tag) }) } func SwidXml(report Report, out string) { - swidZip(out, report, func(tag *swid.SoftwareIdentity, w io.Writer) { - xml.NewEncoder(w).Encode(tag) + swidZip(out, report, func(tag *swid.SoftwareIdentity, w io.Writer) error { + return xml.NewEncoder(w).Encode(tag) }) } diff --git a/cmd/format/xml.go b/cmd/format/xml.go index c56473e5..5b7e4d00 100644 --- a/cmd/format/xml.go +++ b/cmd/format/xml.go @@ -6,7 +6,7 @@ import ( ) func Xml(report Report, out string) { - outWrite(out, func(w io.Writer) { - xml.NewEncoder(w).Encode(report) + outWrite(out, func(w io.Writer) error { + return xml.NewEncoder(w).Encode(report) }) } diff --git a/main.go b/main.go index c1567b67..256818f3 100644 --- a/main.go +++ b/main.go @@ -103,7 +103,7 @@ func args() { flag.BoolVar(&login, "login", false, "login to cloud server. example: -login") flag.StringVar(&cfgf, "config", "", "config path. example: -config config.json") flag.StringVar(&cfg.Path, "path", cfg.Path, "project path. example: -path project_path") - flag.StringVar(&cfg.Output, "out", cfg.Output, "report path, support html/json/xml/csv/sqlite/cdx/spdx/swid/dsdx. example: -out out.json,out.html") + flag.StringVar(&cfg.Output, "out", cfg.Output, "report path, support html/json/xml/csv/sarif/sqlite/cdx/spdx/swid/dsdx. example: -out out.json,out.html") flag.StringVar(&cfg.LogFile, "log", cfg.LogFile, "-log ./my_opensca_log.txt") flag.StringVar(&cfg.Origin.Token, "token", "", "web token, example: -token xxxx") flag.StringVar(&proj, "proj", proj, "saas project id, example: -proj xxxx")