Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/report/sarif.go

333 lines
7.9 KiB
Go

/*
©AngelaMos | 2026
sarif.go
SARIF 2.1.0 reporter for GitHub Advanced Security and IDE integration
Builds a SARIF run with deduplicated rule entries and individual result
records. Severity maps to SARIF levels (error/warning/note) and numeric
security-severity scores. Container targets use docker:// URIs; file
targets use their path directly. Output is capped at SARIFMaxResults.
Key exports:
SARIFReporter - implements Reporter for SARIF output
Connects to:
reporter.go - implements Reporter interface, returned by NewReporter
config/constants.go - reads SARIFMaxResults
finding.go - converts Finding and Collection to SARIF structures
*/
package report
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"time"
"github.com/CarterPerez-dev/docksec/internal/config"
"github.com/CarterPerez-dev/docksec/internal/finding"
)
type SARIFReporter struct {
w io.Writer
closer func() error
}
type sarifReport struct {
Schema string `json:"$schema"`
Version string `json:"version"`
Runs []sarifRun `json:"runs"`
}
type sarifRun struct {
Tool sarifTool `json:"tool"`
Results []sarifResult `json:"results"`
Invocations []sarifInvocation `json:"invocations,omitempty"`
}
type sarifTool struct {
Driver sarifDriver `json:"driver"`
}
type sarifDriver struct {
Name string `json:"name"`
Version string `json:"version"`
InformationURI string `json:"informationUri"`
Rules []sarifRule `json:"rules"`
}
type sarifRule struct {
ID string `json:"id"`
Name string `json:"name"`
ShortDescription sarifMessage `json:"shortDescription"`
FullDescription sarifMessage `json:"fullDescription,omitempty"`
Help sarifMessage `json:"help,omitempty"`
HelpURI string `json:"helpUri,omitempty"`
DefaultConfig sarifDefaultConfig `json:"defaultConfiguration"`
Properties sarifRuleProperties `json:"properties,omitempty"`
}
type sarifDefaultConfig struct {
Level string `json:"level"`
}
type sarifRuleProperties struct {
Tags []string `json:"tags,omitempty"`
SecuritySeverity string `json:"security-severity,omitempty"`
}
type sarifMessage struct {
Text string `json:"text"`
}
type sarifResult struct {
RuleID string `json:"ruleId"`
RuleIndex int `json:"ruleIndex"`
Level string `json:"level"`
Message sarifMessage `json:"message"`
Locations []sarifLocation `json:"locations,omitempty"`
PartialFingerprints map[string]string `json:"partialFingerprints,omitempty"`
Properties sarifResultProperties `json:"properties,omitempty"`
}
type sarifResultProperties struct {
SecuritySeverity string `json:"security-severity,omitempty"`
}
type sarifLocation struct {
PhysicalLocation sarifPhysicalLocation `json:"physicalLocation"`
}
type sarifPhysicalLocation struct {
ArtifactLocation sarifArtifactLocation `json:"artifactLocation"`
Region *sarifRegion `json:"region,omitempty"`
}
type sarifArtifactLocation struct {
URI string `json:"uri"`
}
type sarifRegion struct {
StartLine int `json:"startLine,omitempty"`
StartColumn int `json:"startColumn,omitempty"`
}
type sarifInvocation struct {
ExecutionSuccessful bool `json:"executionSuccessful"`
EndTimeUTC string `json:"endTimeUtc"`
}
func (r *SARIFReporter) Report(findings finding.Collection) error {
defer func() {
if r.closer != nil {
_ = r.closer()
}
}()
report := r.buildReport(findings)
enc := json.NewEncoder(r.w)
enc.SetIndent("", " ")
enc.SetEscapeHTML(false)
return enc.Encode(report)
}
func (r *SARIFReporter) buildReport(findings finding.Collection) sarifReport {
rulesMap := make(map[string]int)
var rules []sarifRule
var results []sarifResult
for _, f := range findings {
if len(results) >= config.SARIFMaxResults {
break
}
ruleIndex, exists := rulesMap[f.RuleID]
if !exists {
ruleIndex = len(rules)
rulesMap[f.RuleID] = ruleIndex
rules = append(rules, r.buildRule(f))
}
results = append(results, r.buildResult(f, ruleIndex))
}
return sarifReport{
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
Version: "2.1.0",
Runs: []sarifRun{
{
Tool: sarifTool{
Driver: sarifDriver{
Name: "docksec",
Version: "1.0.0",
InformationURI: "https://github.com/angelamos/docksec",
Rules: rules,
},
},
Results: results,
Invocations: []sarifInvocation{
{
ExecutionSuccessful: true,
EndTimeUTC: time.Now().
UTC().
Format(time.RFC3339),
},
},
},
},
}
}
func (r *SARIFReporter) buildRule(f *finding.Finding) sarifRule {
rule := sarifRule{
ID: f.RuleID,
Name: f.RuleID,
ShortDescription: sarifMessage{
Text: f.Title,
},
DefaultConfig: sarifDefaultConfig{
Level: r.severityToLevel(f.Severity),
},
Properties: sarifRuleProperties{
SecuritySeverity: r.severityToScore(f.Severity),
Tags: []string{"security", "docker"},
},
}
if f.Description != "" {
rule.FullDescription = sarifMessage{Text: f.Description}
}
if f.Remediation != "" {
rule.Help = sarifMessage{Text: f.Remediation}
}
if len(f.References) > 0 {
rule.HelpURI = f.References[0]
}
if f.CISControl != nil {
rule.Properties.Tags = append(
rule.Properties.Tags,
"CIS",
"CIS-"+f.CISControl.ID,
)
}
return rule
}
func (r *SARIFReporter) buildResult(
f *finding.Finding,
ruleIndex int,
) sarifResult {
result := sarifResult{
RuleID: f.RuleID,
RuleIndex: ruleIndex,
Level: r.severityToLevel(f.Severity),
Message: sarifMessage{
Text: r.buildMessage(f),
},
PartialFingerprints: map[string]string{
"primaryLocationLineHash": r.fingerprint(f),
},
Properties: sarifResultProperties{
SecuritySeverity: r.severityToScore(f.Severity),
},
}
uri := r.buildURI(f)
if uri != "" {
loc := sarifLocation{
PhysicalLocation: sarifPhysicalLocation{
ArtifactLocation: sarifArtifactLocation{
URI: uri,
},
},
}
if f.Location != nil && f.Location.Line > 0 {
loc.PhysicalLocation.Region = &sarifRegion{
StartLine: f.Location.Line,
}
if f.Location.Column > 0 {
loc.PhysicalLocation.Region.StartColumn = f.Location.Column
}
}
result.Locations = []sarifLocation{loc}
}
return result
}
func (r *SARIFReporter) buildMessage(f *finding.Finding) string {
msg := f.Title
if f.Description != "" {
msg += ": " + f.Description
}
return msg
}
func (r *SARIFReporter) buildURI(f *finding.Finding) string {
if f.Location != nil && f.Location.Path != "" {
return f.Location.Path
}
switch f.Target.Type {
case finding.TargetDockerfile, finding.TargetCompose:
return f.Target.Name
case finding.TargetContainer, finding.TargetImage:
return fmt.Sprintf("docker://%s/%s", f.Target.Type, f.Target.Name)
case finding.TargetDaemon:
return "docker://daemon"
}
return ""
}
func (r *SARIFReporter) fingerprint(f *finding.Finding) string {
data := f.RuleID + "|" + string(f.Target.Type) + "|" + f.Target.Name
if f.Location != nil {
data += fmt.Sprintf("|%s:%d", f.Location.Path, f.Location.Line)
}
hash := sha256.Sum256([]byte(data))
return hex.EncodeToString(hash[:16])
}
func (r *SARIFReporter) severityToLevel(sev finding.Severity) string {
switch sev {
case finding.SeverityCritical, finding.SeverityHigh:
return "error"
case finding.SeverityMedium:
return "warning"
case finding.SeverityLow, finding.SeverityInfo:
return "note"
default:
return "none"
}
}
func (r *SARIFReporter) severityToScore(sev finding.Severity) string {
switch sev {
case finding.SeverityCritical:
return "9.0"
case finding.SeverityHigh:
return "7.0"
case finding.SeverityMedium:
return "5.0"
case finding.SeverityLow:
return "3.0"
case finding.SeverityInfo:
return "1.0"
default:
return "0.0"
}
}