Cybersecurity-Projects/PROJECTS/intermediate/docker-security-audit/internal/report/json.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
}