From c540ce806a87d4126a21fe93c0e32920e16ec472 Mon Sep 17 00:00:00 2001 From: Brandon Palm Date: Thu, 18 Jun 2026 12:31:22 -0500 Subject: [PATCH] CNF-23887: Populate JUnit XML timestamp, hostname, and duration The JUnit XML testsuite element was missing standard attributes that CI systems expect. Add timestamp, hostname, and time attributes, and expose scan duration in both JUnit XML and JSON output. - Add Duration and DurationSeconds fields to ScanResults - Compute duration centrally in assembleResults - Populate testsuite timestamp, hostname, time in JUnit XML - Add hostname() helper with os.Hostname fallback --- internal/output/json_test.go | 9 ++++++--- internal/output/junit.go | 15 ++++++++++++++- internal/output/junit_test.go | 9 +++++++++ internal/scanner/scanner.go | 13 +++++++------ internal/scanner/types.go | 4 ++++ 5 files changed, 40 insertions(+), 10 deletions(-) diff --git a/internal/output/json_test.go b/internal/output/json_test.go index 74c9b17c..7250c5d5 100644 --- a/internal/output/json_test.go +++ b/internal/output/json_test.go @@ -5,6 +5,7 @@ import ( "os" "path/filepath" "testing" + "time" "github.com/openshift/tls-scanner/internal/k8s" "github.com/openshift/tls-scanner/internal/scanner" @@ -12,9 +13,11 @@ import ( func testScanResults() scanner.ScanResults { return scanner.ScanResults{ - Timestamp: "2026-05-13T12:00:00Z", - TotalIPs: 1, - ScannedIPs: 1, + Timestamp: "2026-05-13T12:00:00Z", + Duration: 5 * time.Second, + DurationSeconds: 5.0, + TotalIPs: 1, + ScannedIPs: 1, IPResults: []scanner.IPResult{{ IP: "10.0.0.1", Status: "scanned", diff --git a/internal/output/junit.go b/internal/output/junit.go index e679f267..66f3ab08 100644 --- a/internal/output/junit.go +++ b/internal/output/junit.go @@ -10,12 +10,22 @@ import ( "github.com/openshift/tls-scanner/internal/scanner" ) +func hostname() string { + h, err := os.Hostname() + if err != nil { + return "unknown" + } + return h +} + type JUnitTestSuite struct { XMLName xml.Name `xml:"testsuite"` Name string `xml:"name,attr"` Tests int `xml:"tests,attr"` Failures int `xml:"failures,attr"` Time float64 `xml:"time,attr"` + Timestamp string `xml:"timestamp,attr,omitempty"` + Hostname string `xml:"hostname,attr,omitempty"` Properties []JUnitProperty `xml:"properties>property,omitempty"` TestCases []JUnitTestCase `xml:"testcase"` } @@ -44,7 +54,10 @@ type JUnitProperty struct { func WriteJUnitOutput(scanResults scanner.ScanResults, filename string, pqcCheck bool) error { testSuite := JUnitTestSuite{ - Name: "TLSSecurityScan", + Name: "TLSSecurityScan", + Time: scanResults.Duration.Seconds(), + Timestamp: scanResults.Timestamp, + Hostname: hostname(), } enforceTLSCompliance := scanner.TLSConfigComplianceFailuresEnforced(scanResults) diff --git a/internal/output/junit_test.go b/internal/output/junit_test.go index ce27d484..5cfd674e 100644 --- a/internal/output/junit_test.go +++ b/internal/output/junit_test.go @@ -45,6 +45,15 @@ func TestWriteJUnitOutputBasic(t *testing.T) { if suite.Failures != 0 { t.Errorf("Failures = %d, want 0", suite.Failures) } + if suite.Timestamp != "2026-05-13T12:00:00Z" { + t.Errorf("Timestamp = %q, want %q", suite.Timestamp, "2026-05-13T12:00:00Z") + } + if suite.Hostname == "" { + t.Error("Hostname should not be empty") + } + if suite.Time != 5.0 { + t.Errorf("Time = %f, want 5.0", suite.Time) + } } func TestWriteJUnitOutputPQCFailures(t *testing.T) { diff --git a/internal/scanner/scanner.go b/internal/scanner/scanner.go index f0c00458..56a3dbd2 100644 --- a/internal/scanner/scanner.go +++ b/internal/scanner/scanner.go @@ -259,16 +259,15 @@ MAX_PARALLEL (testssl): %d results := assembleResults(startTime, totalIPs, tlsConfig, discovery.Skipped, batchResults) - duration := time.Since(startTime) fmt.Printf("\n========================================\n") fmt.Printf("CLUSTER SCAN COMPLETE!\n") fmt.Printf("========================================\n") fmt.Printf("Total IPs processed: %d\n", results.ScannedIPs) fmt.Printf("Total ports scanned: %d\n", len(batchResults)) fmt.Printf("Total ports skipped: %d\n", len(discovery.Skipped)) - fmt.Printf("Total time: %v\n", duration) + fmt.Printf("Total time: %v\n", results.Duration) if len(batchResults) > 0 { - fmt.Printf("Throughput: %.2f ports/min\n", float64(len(batchResults))/duration.Minutes()) + fmt.Printf("Throughput: %.2f ports/min\n", float64(len(batchResults))/results.Duration.Minutes()) } fmt.Printf("========================================\n") @@ -295,15 +294,14 @@ func Scan(jobs []ScanJob, concurrentScans int, client *k8s.Client, tlsConfig *k8 batchResults := batchScan(jobs, concurrentScans, client, tlsConfig, policy, timeouts, starttlsPorts) results := assembleResults(startTime, 0, tlsConfig, batchResults) - duration := time.Since(startTime) fmt.Printf("\n========================================\n") fmt.Printf("SCAN COMPLETE!\n") fmt.Printf("========================================\n") fmt.Printf("Total IPs processed: %d\n", results.ScannedIPs) fmt.Printf("Total targets: %d\n", len(jobs)) - fmt.Printf("Total time: %v\n", duration) + fmt.Printf("Total time: %v\n", results.Duration) if results.ScannedIPs > 0 { - fmt.Printf("Average time per host: %.2fs\n", duration.Seconds()/float64(results.ScannedIPs)) + fmt.Printf("Average time per host: %.2fs\n", results.Duration.Seconds()/float64(results.ScannedIPs)) } fmt.Printf("========================================\n") @@ -554,8 +552,11 @@ func assembleResults(startTime time.Time, totalIPs int, tlsConfig *k8s.TLSSecuri totalIPs = len(ipResultMap) } + elapsed := time.Since(startTime) results := ScanResults{ Timestamp: startTime.Format(time.RFC3339), + Duration: elapsed, + DurationSeconds: elapsed.Seconds(), TotalIPs: totalIPs, IPResults: make([]IPResult, 0, len(ipResultMap)), TLSSecurityConfig: tlsConfig, diff --git a/internal/scanner/types.go b/internal/scanner/types.go index f97daa77..e496a048 100644 --- a/internal/scanner/types.go +++ b/internal/scanner/types.go @@ -1,6 +1,8 @@ package scanner import ( + "time" + "github.com/openshift/tls-scanner/internal/k8s" ) @@ -54,6 +56,8 @@ type Elem struct { type ScanResults struct { Timestamp string `json:"timestamp"` + DurationSeconds float64 `json:"duration_seconds"` + Duration time.Duration `json:"-"` TotalIPs int `json:"total_ips"` ScannedIPs int `json:"scanned_ips"` IPResults []IPResult `json:"ip_results"`