Skip to content

Commit 2ada35f

Browse files
authored
Merge pull request #248 from luotianqi777/sarif
feat: support sarif
2 parents c069eaa + eeb2dab commit 2ada35f

14 files changed

Lines changed: 256 additions & 42 deletions

File tree

.github/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ Files supported by the `out` parameter are listed below:
131131
| | `html` | `.html` | `v1.0.6` and above |
132132
| | `sqlite` | `.sqlite` | `v1.0.13` and above|
133133
| | `csv` | `.csv` | `v1.0.13` and above|
134+
| | `sarif`| `.sarif` | |
134135
| SBOM | `spdx` | `.spdx` `.spdx.json` `.spdx.xml` | `v1.0.8` and above |
135136
| | `cdx` | `.cdx.json` `.cdx.xml` | `v1.0.11`and above |
136137
| | `swid` | `.swid.json` `.swid.xml` | `v1.0.11`and above |

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ v3.0.2开始,OpenSCA-cli可以通过proj参数向OpenSCA SaaS同步检出结
126126
| | `html` | `.html` |
127127
| | `sqlite` | `.sqlite` |
128128
| | `csv` | `.csv` |
129+
| | `sarif` | `.sarif` |
129130
| SBOM清单 | `spdx` | `.spdx` `.spdx.json` `.spdx.xml` |
130131
| | `cdx` | `.cdx.json` `.cdx.xml` |
131132
| | `swid` | `.swid.json` `.swid.xml` |

cmd/detail/detail.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,20 @@ type Vuln struct {
156156
ExploitLevelId int `json:"exploit_level_id" gorm:"column:exploit_level_id"`
157157
}
158158

159+
func (v *Vuln) SecurityLevel() string {
160+
switch v.SecurityLevelId {
161+
case 1:
162+
return "Critical"
163+
case 2:
164+
return "High"
165+
case 3:
166+
return "Medium"
167+
case 4:
168+
return "Low"
169+
}
170+
return "Unknown"
171+
}
172+
159173
func vulnLanguageKey(language model.Language) []string {
160174
switch language {
161175
case model.Lan_Java:

cmd/format/csv.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ func Csv(report Report, out string) {
2525
return true
2626
})
2727

28-
outWrite(out, func(w io.Writer) {
29-
w.Write([]byte(table))
28+
outWrite(out, func(w io.Writer) error {
29+
_, err := w.Write([]byte(table))
30+
return err
3031
})
3132

3233
}

cmd/format/cyclonedx.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,14 +58,14 @@ func cyclonedxbom(dep *detail.DepDetailGraph) *cyclonedx.BOM {
5858

5959
func CycloneDXJson(report Report, out string) {
6060
bom := cyclonedxbom(report.DepDetailGraph)
61-
outWrite(out, func(w io.Writer) {
62-
cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatJSON).SetPretty(true).Encode(bom)
61+
outWrite(out, func(w io.Writer) error {
62+
return cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatJSON).SetPretty(true).Encode(bom)
6363
})
6464
}
6565

6666
func CycloneDXXml(report Report, out string) {
6767
bom := cyclonedxbom(report.DepDetailGraph)
68-
outWrite(out, func(w io.Writer) {
69-
cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatXML).SetPretty(true).Encode(bom)
68+
outWrite(out, func(w io.Writer) error {
69+
return cyclonedx.NewBOMEncoder(w, cyclonedx.BOMFileFormatXML).SetPretty(true).Encode(bom)
7070
})
7171
}

cmd/format/dsdx.go

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,28 +6,24 @@ import (
66
"io"
77

88
"github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail"
9-
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/logs"
109
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/model"
1110
)
1211

1312
func Dsdx(report Report, out string) {
14-
outWrite(out, func(w io.Writer) {
15-
err := dsdxDoc(report).WriteDsdx(w)
16-
if err != nil {
17-
logs.Warn(err)
18-
}
13+
outWrite(out, func(w io.Writer) error {
14+
return dsdxDoc(report).WriteDsdx(w)
1915
})
2016
}
2117

2218
func DsdxJson(report Report, out string) {
23-
outWrite(out, func(w io.Writer) {
24-
json.NewEncoder(w).Encode(dsdxDoc(report))
19+
outWrite(out, func(w io.Writer) error {
20+
return json.NewEncoder(w).Encode(dsdxDoc(report))
2521
})
2622
}
2723

2824
func DsdxXml(report Report, out string) {
29-
outWrite(out, func(w io.Writer) {
30-
xml.NewEncoder(w).Encode(dsdxDoc(report))
25+
outWrite(out, func(w io.Writer) error {
26+
return xml.NewEncoder(w).Encode(dsdxDoc(report))
3127
})
3228
}
3329

cmd/format/html.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -86,8 +86,9 @@ func Html(report Report, out string) {
8686
}); err != nil {
8787
logs.Warn(err)
8888
} else {
89-
outWrite(out, func(w io.Writer) {
90-
w.Write(bytes.Replace(index, []byte(`"此处填充json数据"`), data, 1))
89+
outWrite(out, func(w io.Writer) error {
90+
_, err := w.Write(bytes.Replace(index, []byte(`"此处填充json数据"`), data, 1))
91+
return err
9192
})
9293
return
9394
}

cmd/format/json.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import (
66
)
77

88
func Json(report Report, out string) {
9-
outWrite(out, func(w io.Writer) {
9+
outWrite(out, func(w io.Writer) error {
1010
encoder := json.NewEncoder(w)
1111
encoder.SetIndent("", " ")
12-
encoder.Encode(report)
12+
return encoder.Encode(report)
1313
})
1414
}

cmd/format/sarif.go

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
package format
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io"
7+
"regexp"
8+
"strings"
9+
10+
"github.com/xmirrorsecurity/opensca-cli/v3/cmd/detail"
11+
)
12+
13+
type sarifReport struct {
14+
Version string `json:"version"`
15+
Schema string `json:"$schema"`
16+
Runs []sarifRun `json:"runs"`
17+
}
18+
19+
type sarifRun struct {
20+
Tool struct {
21+
Driver struct {
22+
Name string `json:"name"`
23+
Version string `json:"version"`
24+
InformationUri string `json:"informationUri"`
25+
Rules []sarifRule `json:"rules"`
26+
} `json:"driver"`
27+
} `json:"tool"`
28+
Results []sarifResult `json:"results"`
29+
}
30+
31+
type sarifRule struct {
32+
Id string `json:"id"`
33+
Name string `json:"name"`
34+
ShortDescription sarifRuleShortDescription `json:"shortDescription"`
35+
FullDescription sarifRuleFullDescription `json:"fullDescription"`
36+
Help sarifRuleHelp `json:"help"`
37+
Properties sarifRuleProperties `json:"properties"`
38+
}
39+
40+
type sarifRuleShortDescription struct {
41+
Text string `json:"text"`
42+
}
43+
44+
type sarifRuleFullDescription struct {
45+
Text string `json:"text"`
46+
}
47+
48+
type sarifRuleHelp struct {
49+
Text string `json:"text"`
50+
Markdown string `json:"markdown"`
51+
}
52+
53+
type sarifRuleProperties struct {
54+
Tags []string `json:"tags"`
55+
}
56+
57+
type sarifResult struct {
58+
RuleId string `json:"ruleId"`
59+
Level string `json:"level"`
60+
Message struct {
61+
Text string `json:"text"`
62+
} `json:"message"`
63+
Locations []sarifLocation `json:"locations"`
64+
}
65+
66+
type sarifLocation struct {
67+
PhysicalLocation struct {
68+
ArtifactLocation struct {
69+
Uri string `json:"uri"`
70+
Index int `json:"index,omitempty"`
71+
} `json:"artifactLocation"`
72+
Region struct {
73+
StartColumn int `json:"startColumn"`
74+
EndColumn int `json:"endColumn"`
75+
StartLine int `json:"startLine"`
76+
EndLine int `json:"endLine"`
77+
} `json:"region"`
78+
} `json:"physicalLocation"`
79+
}
80+
81+
func Sarif(report Report, out string) {
82+
83+
s := sarifReport{
84+
Version: "2.1.0",
85+
Schema: "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
86+
}
87+
88+
run := sarifRun{}
89+
run.Tool.Driver.Name = "opensca-cli"
90+
run.Tool.Driver.Version = strings.TrimLeft(report.TaskInfo.ToolVersion, "vV")
91+
run.Tool.Driver.InformationUri = "https://opensca.xmirror.cn"
92+
93+
vulnInfos := map[string]*detail.VulnInfo{}
94+
95+
report.ForEach(func(n *detail.DepDetailGraph) bool {
96+
for _, vuln := range n.Vulnerabilities {
97+
98+
if vuln.Id == "" {
99+
continue
100+
}
101+
102+
vulnInfos[vuln.Id] = &detail.VulnInfo{Vuln: vuln, Language: n.Language}
103+
104+
result := sarifResult{
105+
RuleId: vuln.Id,
106+
Level: "warning",
107+
}
108+
result.Message.Text = fmt.Sprintf("引入的组件 %s 中存在 %s", n.Dep.Key()[:strings.LastIndex(n.Dep.Key(), ":")], vuln.Name)
109+
for i, path := range n.Paths {
110+
if truncIndex := strings.Index(path, "["); truncIndex > 0 {
111+
path = strings.Trim(path[:truncIndex], `\/`)
112+
}
113+
location := sarifLocation{}
114+
location.PhysicalLocation.ArtifactLocation.Uri = path
115+
location.PhysicalLocation.ArtifactLocation.Index = i
116+
location.PhysicalLocation.Region.StartColumn = 1
117+
location.PhysicalLocation.Region.EndColumn = 1
118+
location.PhysicalLocation.Region.StartLine = 1
119+
location.PhysicalLocation.Region.EndLine = 1
120+
result.Locations = append(result.Locations, location)
121+
}
122+
123+
run.Results = append(run.Results, result)
124+
}
125+
return true
126+
})
127+
128+
for _, vuln := range vulnInfos {
129+
run.Tool.Driver.Rules = append(run.Tool.Driver.Rules, sarifRule{
130+
Id: vuln.Id,
131+
Name: vuln.Name,
132+
ShortDescription: sarifRuleShortDescription{Text: vuln.Name},
133+
FullDescription: sarifRuleFullDescription{Text: vuln.Description},
134+
Help: sarifRuleHelp{Markdown: formatDesc(vuln)},
135+
Properties: sarifRuleProperties{Tags: formatTags(vuln)},
136+
})
137+
}
138+
139+
s.Runs = []sarifRun{run}
140+
outWrite(out, func(w io.Writer) error {
141+
return json.NewEncoder(w).Encode(s)
142+
})
143+
}
144+
145+
func formatDesc(v *detail.VulnInfo) string {
146+
table := []struct {
147+
fmt string
148+
val string
149+
}{
150+
{"| id | %s |", v.Id},
151+
{"| --- | --- |", ""},
152+
{"| cve | %s |", v.Cve},
153+
{"| cnnvd | %s |", v.Cnnvd},
154+
{"| cnvd | %s |", v.Cnvd},
155+
{"| cwe | %s |", v.Cwe},
156+
{"| level | %s |", v.SecurityLevel()},
157+
{"| desc | %s |", sanitizeString(v.Description)},
158+
{"| suggestion | %s |", sanitizeString(v.Suggestion)},
159+
}
160+
var lines []string
161+
for _, line := range table {
162+
if strings.Contains(line.fmt, "%s") && line.val == "" {
163+
continue
164+
}
165+
if line.val == "" {
166+
lines = append(lines, line.fmt)
167+
} else {
168+
lines = append(lines, fmt.Sprintf(line.fmt, line.val))
169+
}
170+
}
171+
172+
return strings.Join(lines, "\n")
173+
}
174+
175+
func sanitizeString(s string) string {
176+
re := regexp.MustCompile("<[^>]*>")
177+
s = re.ReplaceAllString(s, "")
178+
179+
s = strings.ReplaceAll(s, "\r", "")
180+
s = strings.ReplaceAll(s, "\n", "")
181+
182+
return s
183+
}
184+
185+
func formatTags(v *detail.VulnInfo) []string {
186+
tags := []string{"security", "Use-Vulnerable-and-Outdated-Components", v.Cve, v.Cwe, v.AttackType, v.Language}
187+
for i := 0; i < len(tags); {
188+
if tags[i] == "" {
189+
tags = append(tags[:i], tags[i+1:]...)
190+
} else {
191+
i++
192+
}
193+
}
194+
return tags
195+
}

cmd/format/save.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,13 +71,15 @@ func Save(report Report, output string) {
7171
Csv(report, out)
7272
case ".sqlite", ".db":
7373
Sqlite(report, out)
74+
case ".sarif":
75+
Sarif(report, out)
7476
default:
7577
Json(report, out)
7678
}
7779
}
7880
}
7981

80-
func outWrite(out string, do func(io.Writer)) {
82+
func outWrite(out string, do func(io.Writer) error) {
8183

8284
if out == "" {
8385
do(os.Stdout)
@@ -95,7 +97,9 @@ func outWrite(out string, do func(io.Writer)) {
9597
logs.Warn(err)
9698
} else {
9799
defer w.Close()
98-
do(w)
100+
if err = do(w); err != nil {
101+
logs.Warn(err)
102+
}
99103
}
100104
}
101105

0 commit comments

Comments
 (0)