161 lines
3.9 KiB
Go
161 lines
3.9 KiB
Go
/*
|
|
©AngelaMos | 2026
|
|
json.go
|
|
|
|
JSON reporter that emits a structured scan result document
|
|
|
|
Builds a top-level document with schema version, timestamp, severity
|
|
summary, and a full finding list. CISControl, Location, and References
|
|
are included when present. Output is indented JSON written via
|
|
json.Encoder with HTML escaping disabled.
|
|
|
|
Key exports:
|
|
JSONReporter - implements Reporter for JSON output
|
|
|
|
Connects to:
|
|
reporter.go - implements Reporter interface, returned by NewReporter
|
|
finding.go - converts Finding and Collection to JSON structures
|
|
*/
|
|
|
|
package report
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"time"
|
|
|
|
"github.com/CarterPerez-dev/docksec/internal/finding"
|
|
)
|
|
|
|
type JSONReporter struct {
|
|
w io.Writer
|
|
closer func() error
|
|
}
|
|
|
|
type jsonReport struct {
|
|
Version string `json:"version"`
|
|
Timestamp string `json:"timestamp"`
|
|
Summary jsonSummary `json:"summary"`
|
|
Findings []jsonFinding `json:"findings"`
|
|
}
|
|
|
|
type jsonSummary struct {
|
|
Total int `json:"total"`
|
|
BySeverity map[string]int `json:"by_severity"`
|
|
}
|
|
|
|
type jsonFinding struct {
|
|
ID string `json:"id"`
|
|
RuleID string `json:"rule_id"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
Severity string `json:"severity"`
|
|
Category string `json:"category,omitempty"`
|
|
Target jsonTarget `json:"target"`
|
|
Location *jsonLocation `json:"location,omitempty"`
|
|
Remediation string `json:"remediation,omitempty"`
|
|
References []string `json:"references,omitempty"`
|
|
CISControl *jsonCIS `json:"cis_control,omitempty"`
|
|
Timestamp string `json:"timestamp"`
|
|
}
|
|
|
|
type jsonTarget struct {
|
|
Type string `json:"type"`
|
|
Name string `json:"name"`
|
|
ID string `json:"id,omitempty"`
|
|
}
|
|
|
|
type jsonLocation struct {
|
|
Path string `json:"path"`
|
|
Line int `json:"line,omitempty"`
|
|
Column int `json:"column,omitempty"`
|
|
}
|
|
|
|
type jsonCIS struct {
|
|
ID string `json:"id"`
|
|
Section string `json:"section,omitempty"`
|
|
Title string `json:"title"`
|
|
Description string `json:"description,omitempty"`
|
|
Scored bool `json:"scored"`
|
|
Level int `json:"level"`
|
|
}
|
|
|
|
func (r *JSONReporter) 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 *JSONReporter) buildReport(findings finding.Collection) jsonReport {
|
|
counts := findings.CountBySeverity()
|
|
bySeverity := make(map[string]int)
|
|
for sev, count := range counts {
|
|
bySeverity[sev.String()] = count
|
|
}
|
|
|
|
jsonFindings := make([]jsonFinding, 0, len(findings))
|
|
for _, f := range findings {
|
|
jsonFindings = append(jsonFindings, r.convertFinding(f))
|
|
}
|
|
|
|
return jsonReport{
|
|
Version: "1.0.0",
|
|
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
|
Summary: jsonSummary{
|
|
Total: len(findings),
|
|
BySeverity: bySeverity,
|
|
},
|
|
Findings: jsonFindings,
|
|
}
|
|
}
|
|
|
|
func (r *JSONReporter) convertFinding(f *finding.Finding) jsonFinding {
|
|
jf := jsonFinding{
|
|
ID: f.ID,
|
|
RuleID: f.RuleID,
|
|
Title: f.Title,
|
|
Description: f.Description,
|
|
Severity: f.Severity.String(),
|
|
Category: f.Category,
|
|
Target: jsonTarget{
|
|
Type: string(f.Target.Type),
|
|
Name: f.Target.Name,
|
|
ID: f.Target.ID,
|
|
},
|
|
Remediation: f.Remediation,
|
|
References: f.References,
|
|
Timestamp: f.Timestamp.UTC().Format(time.RFC3339),
|
|
}
|
|
|
|
if f.Location != nil {
|
|
jf.Location = &jsonLocation{
|
|
Path: f.Location.Path,
|
|
Line: f.Location.Line,
|
|
Column: f.Location.Column,
|
|
}
|
|
}
|
|
|
|
if f.CISControl != nil {
|
|
jf.CISControl = &jsonCIS{
|
|
ID: f.CISControl.ID,
|
|
Section: f.CISControl.Section,
|
|
Title: f.CISControl.Title,
|
|
Description: f.CISControl.Description,
|
|
Scored: f.CISControl.Scored,
|
|
Level: f.CISControl.Level,
|
|
}
|
|
}
|
|
|
|
return jf
|
|
}
|