gitex2026/AttackSurface/src/gotestwaf/internal/report/console.go
2026-04-24 12:36:21 +00:00

346 lines
11 KiB
Go

package report
import (
"encoding/json"
"fmt"
"strings"
"time"
"github.com/olekukonko/tablewriter"
"github.com/pkg/errors"
"github.com/wallarm/gotestwaf/internal/db"
)
// The minimum length of each column in a console table report.
const colMinWidth = 21
// RenderConsoleReport prints a console report in selected format.
func RenderConsoleReport(
s *db.Statistics,
reportTime time.Time,
wafName string,
url string,
args []string,
ignoreUnresolved bool,
format string,
) error {
switch format {
case consoleReportTextFormat:
printConsoleReportTable(s, reportTime, wafName, ignoreUnresolved)
case consoleReportJsonFormat:
err := printConsoleReportJson(s, reportTime, wafName, url, args)
if err != nil {
return err
}
default:
return fmt.Errorf("unknown report format: %s", format)
}
return nil
}
// printConsoleReportTable prepare and prints a console report in tabular format.
func printConsoleReportTable(
s *db.Statistics,
reportTime time.Time,
wafName string,
ignoreUnresolved bool,
) {
baseHeader := []string{"Test set", "Test case", "Percentage, %", "Blocked", "Bypassed"}
if !ignoreUnresolved {
baseHeader = append(baseHeader, "Unresolved")
}
baseHeader = append(baseHeader, "Sent", "Failed")
var buffer strings.Builder
fmt.Fprintf(&buffer, "True-Positive Tests:\n")
// Negative cases summary table
table := tablewriter.NewWriter(&buffer)
table.Header(baseHeader)
for _, row := range s.TruePositiveTests.SummaryTable {
rowAppend := []string{
row.TestSet,
row.TestCase,
fmt.Sprintf("%.2f", row.Percentage),
fmt.Sprintf("%d", row.Blocked),
fmt.Sprintf("%d", row.Bypassed),
}
if !ignoreUnresolved {
rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
}
rowAppend = append(rowAppend,
fmt.Sprintf("%d", row.Sent),
fmt.Sprintf("%d", row.Failed),
)
table.Append(rowAppend)
}
footerNegativeTests := []string{
fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
fmt.Sprintf("Project Name:\n%s", wafName),
fmt.Sprintf("True-Positive Score:\n%.2f%%", s.TruePositiveTests.ResolvedBlockedRequestsPercentage),
fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
),
fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
s.TruePositiveTests.ResolvedBypassedRequestsPercentage,
),
}
if !ignoreUnresolved {
footerNegativeTests = append(footerNegativeTests,
fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
s.TruePositiveTests.ReqStats.AllRequestsNumber,
s.TruePositiveTests.UnresolvedRequestsPercentage,
),
)
}
footerNegativeTests = append(footerNegativeTests,
fmt.Sprintf("Total Sent:\n%d", s.TruePositiveTests.ReqStats.AllRequestsNumber),
fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
s.TruePositiveTests.ReqStats.FailedRequestsNumber,
s.TruePositiveTests.ReqStats.AllRequestsNumber,
s.TruePositiveTests.FailedRequestsPercentage,
),
)
table.Footer(footerNegativeTests)
table.Render()
fmt.Fprintf(&buffer, "\nTrue-Negative Tests:\n")
// Positive cases summary table
posTable := tablewriter.NewWriter(&buffer)
posTable.Header(baseHeader)
for _, row := range s.TrueNegativeTests.SummaryTable {
rowAppend := []string{
row.TestSet,
row.TestCase,
fmt.Sprintf("%.2f", row.Percentage),
fmt.Sprintf("%d", row.Blocked),
fmt.Sprintf("%d", row.Bypassed),
}
if !ignoreUnresolved {
rowAppend = append(rowAppend, fmt.Sprintf("%d", row.Unresolved))
}
rowAppend = append(rowAppend,
fmt.Sprintf("%d", row.Sent),
fmt.Sprintf("%d", row.Failed),
)
posTable.Append(rowAppend)
}
footerPositiveTests := []string{
fmt.Sprintf("Date:\n%s", reportTime.Format("2006-01-02")),
fmt.Sprintf("Project Name:\n%s", wafName),
fmt.Sprintf("True-Negative Score:\n%.2f%%", s.TrueNegativeTests.ResolvedBypassedRequestsPercentage),
fmt.Sprintf("Blocked (Resolved):\n%d/%d (%.2f%%)",
s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
s.TrueNegativeTests.ResolvedBlockedRequestsPercentage,
),
fmt.Sprintf("Bypassed (Resolved):\n%d/%d (%.2f%%)",
s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
),
}
if !ignoreUnresolved {
footerPositiveTests = append(footerPositiveTests,
fmt.Sprintf("Unresolved (Sent):\n%d/%d (%.2f%%)",
s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
s.TrueNegativeTests.ReqStats.AllRequestsNumber,
s.TrueNegativeTests.UnresolvedRequestsPercentage,
),
)
}
footerPositiveTests = append(footerPositiveTests,
fmt.Sprintf("Total Sent:\n%d", s.TrueNegativeTests.ReqStats.AllRequestsNumber),
fmt.Sprintf("Failed (Total):\n%d/%d (%.2f%%)",
s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
s.TrueNegativeTests.ReqStats.AllRequestsNumber,
s.TrueNegativeTests.FailedRequestsPercentage,
),
)
posTable.Footer(footerPositiveTests)
posTable.Render()
fmt.Fprintf(&buffer, "\nSummary:\n")
// summary table
sumTable := tablewriter.NewWriter(&buffer)
baseHeader = []string{"Type", "True-Positive tests blocked", "True-Negative tests passed", "Average"}
sumTable.Header(baseHeader)
row := []string{"API Security"}
if s.Score.ApiSec.TruePositive != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TruePositive))
} else {
row = append(row, "n/a")
}
if s.Score.ApiSec.TrueNegative != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.TrueNegative))
} else {
row = append(row, "n/a")
}
if s.Score.ApiSec.Average != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.ApiSec.Average))
} else {
row = append(row, "n/a")
}
sumTable.Append(row)
row = []string{"Application Security"}
if s.Score.AppSec.TruePositive != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TruePositive))
} else {
row = append(row, "n/a")
}
if s.Score.AppSec.TrueNegative != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.TrueNegative))
} else {
row = append(row, "n/a")
}
if s.Score.AppSec.Average != -1.0 {
row = append(row, fmt.Sprintf("%.2f%%", s.Score.AppSec.Average))
} else {
row = append(row, "n/a")
}
sumTable.Append(row)
footer := []string{"", "", "Score"}
if s.Score.Average != -1.0 {
footer = append(footer, fmt.Sprintf("%.2f%%", s.Score.Average))
} else {
footer = append(footer, "n/a")
}
sumTable.Footer(footer)
sumTable.Render()
fmt.Println(buffer.String())
}
// printConsoleReportJson prepares and prints a console report in json format.
func printConsoleReportJson(
s *db.Statistics,
reportTime time.Time,
wafName string,
url string,
args []string,
) error {
report := jsonReport{
Date: reportTime.Format(time.ANSIC),
ProjectName: wafName,
URL: url,
TestCasesFP: s.TestCasesFingerprint,
Args: strings.Join(args, " "),
Score: s.Score.Average,
}
if len(s.TruePositiveTests.SummaryTable) != 0 {
report.TruePositiveTests = &testsInfo{
Score: s.TruePositiveTests.ResolvedBlockedRequestsPercentage,
Summary: requestStats{
TotalSent: s.TruePositiveTests.ReqStats.AllRequestsNumber,
ResolvedTests: s.TruePositiveTests.ReqStats.ResolvedRequestsNumber,
BlockedTests: s.TruePositiveTests.ReqStats.BlockedRequestsNumber,
BypassedTests: s.TruePositiveTests.ReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TruePositiveTests.ReqStats.UnresolvedRequestsNumber,
FailedTests: s.TruePositiveTests.ReqStats.FailedRequestsNumber,
},
ApiSecStat: requestStats{
TotalSent: s.TruePositiveTests.ApiSecReqStats.AllRequestsNumber,
ResolvedTests: s.TruePositiveTests.ApiSecReqStats.ResolvedRequestsNumber,
BlockedTests: s.TruePositiveTests.ApiSecReqStats.BlockedRequestsNumber,
BypassedTests: s.TruePositiveTests.ApiSecReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TruePositiveTests.ApiSecReqStats.UnresolvedRequestsNumber,
FailedTests: s.TruePositiveTests.ApiSecReqStats.FailedRequestsNumber,
},
AppSecStat: requestStats{
TotalSent: s.TruePositiveTests.AppSecReqStats.AllRequestsNumber,
ResolvedTests: s.TruePositiveTests.AppSecReqStats.ResolvedRequestsNumber,
BlockedTests: s.TruePositiveTests.AppSecReqStats.BlockedRequestsNumber,
BypassedTests: s.TruePositiveTests.AppSecReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TruePositiveTests.AppSecReqStats.UnresolvedRequestsNumber,
FailedTests: s.TruePositiveTests.AppSecReqStats.FailedRequestsNumber,
},
TestSets: make(testSets),
}
for _, row := range s.TruePositiveTests.SummaryTable {
if report.TruePositiveTests.TestSets[row.TestSet] == nil {
report.TruePositiveTests.TestSets[row.TestSet] = make(testCases)
}
report.TruePositiveTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
Percentage: row.Percentage,
Sent: row.Sent,
Blocked: row.Blocked,
Bypassed: row.Bypassed,
Unresolved: row.Unresolved,
Failed: row.Failed,
}
}
}
if len(s.TrueNegativeTests.SummaryTable) != 0 {
report.TrueNegativeTests = &testsInfo{
Score: s.TrueNegativeTests.ResolvedBypassedRequestsPercentage,
Summary: requestStats{
TotalSent: s.TrueNegativeTests.ReqStats.AllRequestsNumber,
ResolvedTests: s.TrueNegativeTests.ReqStats.ResolvedRequestsNumber,
BlockedTests: s.TrueNegativeTests.ReqStats.BlockedRequestsNumber,
BypassedTests: s.TrueNegativeTests.ReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TrueNegativeTests.ReqStats.UnresolvedRequestsNumber,
FailedTests: s.TrueNegativeTests.ReqStats.FailedRequestsNumber,
},
ApiSecStat: requestStats{
TotalSent: s.TrueNegativeTests.ApiSecReqStats.AllRequestsNumber,
ResolvedTests: s.TrueNegativeTests.ApiSecReqStats.ResolvedRequestsNumber,
BlockedTests: s.TrueNegativeTests.ApiSecReqStats.BlockedRequestsNumber,
BypassedTests: s.TrueNegativeTests.ApiSecReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TrueNegativeTests.ApiSecReqStats.UnresolvedRequestsNumber,
FailedTests: s.TrueNegativeTests.ApiSecReqStats.FailedRequestsNumber,
},
AppSecStat: requestStats{
TotalSent: s.TrueNegativeTests.AppSecReqStats.AllRequestsNumber,
ResolvedTests: s.TrueNegativeTests.AppSecReqStats.ResolvedRequestsNumber,
BlockedTests: s.TrueNegativeTests.AppSecReqStats.BlockedRequestsNumber,
BypassedTests: s.TrueNegativeTests.AppSecReqStats.BypassedRequestsNumber,
UnresolvedTests: s.TrueNegativeTests.AppSecReqStats.UnresolvedRequestsNumber,
FailedTests: s.TrueNegativeTests.AppSecReqStats.FailedRequestsNumber,
},
TestSets: make(testSets),
}
for _, row := range s.TrueNegativeTests.SummaryTable {
if report.TrueNegativeTests.TestSets[row.TestSet] == nil {
report.TrueNegativeTests.TestSets[row.TestSet] = make(testCases)
}
report.TrueNegativeTests.TestSets[row.TestSet][row.TestCase] = &testCaseInfo{
Percentage: row.Percentage,
Sent: row.Sent,
Blocked: row.Blocked,
Bypassed: row.Bypassed,
Unresolved: row.Unresolved,
Failed: row.Failed,
}
}
}
jsonBytes, err := json.Marshal(report)
if err != nil {
return errors.Wrap(err, "couldn't export report to JSON")
}
fmt.Println(string(jsonBytes))
return nil
}