Skip to content

Commit 271d646

Browse files
committed
feat: add 35 static analysis rules and SARIF output
1 parent e6afca4 commit 271d646

3 files changed

Lines changed: 1383 additions & 0 deletions

File tree

sarif.go

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
package sight
2+
3+
import (
4+
"encoding/json"
5+
"strings"
6+
)
7+
8+
// SARIF 2.1.0 output support.
9+
// This enables integration with GitHub Code Scanning, VS Code SARIF Viewer,
10+
// and other tools that consume the OASIS SARIF standard.
11+
12+
// sarifLog is the top-level SARIF 2.1.0 structure.
13+
type sarifLog struct {
14+
Schema string `json:"$schema"`
15+
Version string `json:"version"`
16+
Runs []sarifRun `json:"runs"`
17+
}
18+
19+
// sarifRun represents a single analysis run.
20+
type sarifRun struct {
21+
Tool sarifTool `json:"tool"`
22+
Results []sarifResult `json:"results"`
23+
}
24+
25+
// sarifTool describes the analysis tool.
26+
type sarifTool struct {
27+
Driver sarifDriver `json:"driver"`
28+
}
29+
30+
// sarifDriver describes the tool driver (primary component).
31+
type sarifDriver struct {
32+
Name string `json:"name"`
33+
Version string `json:"version"`
34+
InformationURI string `json:"informationUri"`
35+
Rules []sarifReportingDesc `json:"rules,omitempty"`
36+
SemanticVersion string `json:"semanticVersion"`
37+
}
38+
39+
// sarifReportingDesc describes a rule in the tool.
40+
type sarifReportingDesc struct {
41+
ID string `json:"id"`
42+
Name string `json:"name,omitempty"`
43+
ShortDescription sarifMultiformat `json:"shortDescription"`
44+
FullDescription sarifMultiformat `json:"fullDescription,omitempty"`
45+
HelpURI string `json:"helpUri,omitempty"`
46+
Help *sarifMultiformat `json:"help,omitempty"`
47+
Properties *sarifRuleProps `json:"properties,omitempty"`
48+
}
49+
50+
// sarifRuleProps holds additional rule metadata.
51+
type sarifRuleProps struct {
52+
Tags []string `json:"tags,omitempty"`
53+
}
54+
55+
// sarifMultiformat is a text/markdown pair.
56+
type sarifMultiformat struct {
57+
Text string `json:"text"`
58+
}
59+
60+
// sarifResult is a single finding in SARIF format.
61+
type sarifResult struct {
62+
RuleID string `json:"ruleId"`
63+
RuleIndex int `json:"ruleIndex"`
64+
Level string `json:"level"`
65+
Message sarifMultiformat `json:"message"`
66+
Locations []sarifLocation `json:"locations,omitempty"`
67+
Fixes []sarifFix `json:"fixes,omitempty"`
68+
}
69+
70+
// sarifLocation describes where a result was found.
71+
type sarifLocation struct {
72+
PhysicalLocation sarifPhysicalLoc `json:"physicalLocation"`
73+
}
74+
75+
// sarifPhysicalLoc has the artifact and region.
76+
type sarifPhysicalLoc struct {
77+
ArtifactLocation sarifArtifactLoc `json:"artifactLocation"`
78+
Region *sarifRegion `json:"region,omitempty"`
79+
}
80+
81+
// sarifArtifactLoc identifies the file.
82+
type sarifArtifactLoc struct {
83+
URI string `json:"uri"`
84+
URIBaseID string `json:"uriBaseId,omitempty"`
85+
}
86+
87+
// sarifRegion identifies the line(s).
88+
type sarifRegion struct {
89+
StartLine int `json:"startLine"`
90+
EndLine int `json:"endLine,omitempty"`
91+
}
92+
93+
// sarifFix describes a potential fix.
94+
type sarifFix struct {
95+
Description sarifMultiformat `json:"description"`
96+
}
97+
98+
// ToSARIF converts a slice of Finding values into a SARIF 2.1.0 JSON string.
99+
// The output is compatible with GitHub Code Scanning, VS Code SARIF Viewer,
100+
// and other SARIF-consuming tools.
101+
func ToSARIF(findings []Finding) string {
102+
// Build rules index from findings
103+
type ruleKey struct {
104+
id string
105+
}
106+
ruleIndex := make(map[string]int)
107+
var rules []sarifReportingDesc
108+
109+
for _, f := range findings {
110+
ruleID := extractRuleID(f.Message)
111+
if ruleID == "" {
112+
ruleID = f.Concern
113+
}
114+
if _, exists := ruleIndex[ruleID]; !exists {
115+
ruleIndex[ruleID] = len(rules)
116+
desc := sarifReportingDesc{
117+
ID: ruleID,
118+
ShortDescription: sarifMultiformat{Text: extractRuleName(f.Message)},
119+
FullDescription: sarifMultiformat{Text: f.Message},
120+
}
121+
if f.CWE != "" {
122+
desc.Properties = &sarifRuleProps{
123+
Tags: []string{"security", f.CWE},
124+
}
125+
desc.HelpURI = "https://cwe.mitre.org/data/definitions/" + strings.TrimPrefix(f.CWE, "CWE-") + ".html"
126+
}
127+
rules = append(rules, desc)
128+
}
129+
}
130+
131+
// Build results
132+
results := make([]sarifResult, 0, len(findings))
133+
for _, f := range findings {
134+
ruleID := extractRuleID(f.Message)
135+
if ruleID == "" {
136+
ruleID = f.Concern
137+
}
138+
idx := ruleIndex[ruleID]
139+
140+
result := sarifResult{
141+
RuleID: ruleID,
142+
RuleIndex: idx,
143+
Level: severityToSARIFLevel(f.Severity),
144+
Message: sarifMultiformat{Text: f.Message},
145+
}
146+
147+
if f.File != "" {
148+
loc := sarifLocation{
149+
PhysicalLocation: sarifPhysicalLoc{
150+
ArtifactLocation: sarifArtifactLoc{
151+
URI: f.File,
152+
URIBaseID: "%SRCROOT%",
153+
},
154+
},
155+
}
156+
if f.Line > 0 {
157+
loc.PhysicalLocation.Region = &sarifRegion{
158+
StartLine: f.Line,
159+
EndLine: f.EndLine,
160+
}
161+
if f.EndLine == 0 {
162+
loc.PhysicalLocation.Region.EndLine = f.Line
163+
}
164+
}
165+
result.Locations = []sarifLocation{loc}
166+
}
167+
168+
if f.Fix != "" {
169+
result.Fixes = []sarifFix{
170+
{Description: sarifMultiformat{Text: f.Fix}},
171+
}
172+
}
173+
174+
results = append(results, result)
175+
}
176+
177+
log := sarifLog{
178+
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/main/sarif-2.1/schema/sarif-schema-2.1.0.json",
179+
Version: "2.1.0",
180+
Runs: []sarifRun{
181+
{
182+
Tool: sarifTool{
183+
Driver: sarifDriver{
184+
Name: "sight",
185+
Version: "1.0.0",
186+
SemanticVersion: "1.0.0",
187+
InformationURI: "https://github.com/GrayCodeAI/sight",
188+
Rules: rules,
189+
},
190+
},
191+
Results: results,
192+
},
193+
},
194+
}
195+
196+
data, err := json.MarshalIndent(log, "", " ")
197+
if err != nil {
198+
return "{}"
199+
}
200+
return string(data)
201+
}
202+
203+
// severityToSARIFLevel maps sight Severity to SARIF level strings.
204+
func severityToSARIFLevel(s Severity) string {
205+
switch s {
206+
case SeverityCritical, SeverityHigh:
207+
return "error"
208+
case SeverityMedium:
209+
return "warning"
210+
case SeverityLow:
211+
return "note"
212+
default:
213+
return "none"
214+
}
215+
}
216+
217+
// extractRuleID pulls the rule ID from a message formatted as "[ID] Name: Description".
218+
func extractRuleID(msg string) string {
219+
if !strings.HasPrefix(msg, "[") {
220+
return ""
221+
}
222+
end := strings.Index(msg, "]")
223+
if end < 0 {
224+
return ""
225+
}
226+
return msg[1:end]
227+
}
228+
229+
// extractRuleName pulls the rule name from a message formatted as "[ID] Name: Description".
230+
func extractRuleName(msg string) string {
231+
end := strings.Index(msg, "]")
232+
if end < 0 {
233+
return msg
234+
}
235+
rest := strings.TrimSpace(msg[end+1:])
236+
colon := strings.Index(rest, ":")
237+
if colon < 0 {
238+
return rest
239+
}
240+
return strings.TrimSpace(rest[:colon])
241+
}

0 commit comments

Comments
 (0)