diff --git a/cmd/discover/api.go b/cmd/discover/api.go index 9ce4108..1ac191c 100644 --- a/cmd/discover/api.go +++ b/cmd/discover/api.go @@ -58,7 +58,7 @@ func NewAPICmd() (apiCmd *cobra.Command) { internalCmd.TrackScanReport(ctx, tracer, reporter) printtable.WellKnownPathsScanReport(reporter) - printtable.ContextualScanReport(reporter) + printtable.FingerprintScanReport(reporter) }, } diff --git a/cmd/discover/domain.go b/cmd/discover/domain.go index ac7d1ba..17b80f1 100644 --- a/cmd/discover/domain.go +++ b/cmd/discover/domain.go @@ -64,7 +64,7 @@ func NewDomainCmd() (domainCmd *cobra.Command) { internalCmd.TrackScanReport(ctx, tracer, reporter) printtable.WellKnownPathsScanReport(reporter) - printtable.ContextualScanReport(reporter) + printtable.FingerprintScanReport(reporter) } }, } diff --git a/internal/cmd/output.go b/internal/cmd/output.go index bdab6f4..4fe8cb6 100644 --- a/internal/cmd/output.go +++ b/internal/cmd/output.go @@ -55,7 +55,8 @@ func PrintOrExportReport(format string, transport string, report *report.Reporte func PrintTable(report *report.Reporter) { printtable.WellKnownPathsScanReport(report) - printtable.ContextualScanReport(report) + printtable.FingerprintScanReport(report) + printtable.DisplayReportSummaryTable(report) printtable.DisplayReportTable(report) } diff --git a/internal/cmd/printtable/fingerprint_table.go b/internal/cmd/printtable/fingerprint_table.go new file mode 100644 index 0000000..bc601d4 --- /dev/null +++ b/internal/cmd/printtable/fingerprint_table.go @@ -0,0 +1,89 @@ +package printtable + +import ( + "fmt" + + "github.com/cerberauth/vulnapi/report" + "github.com/cerberauth/vulnapi/scan/discover/fingerprint" + "github.com/olekukonko/tablewriter" +) + +func FingerprintScanReport(reporter *report.Reporter) { + report := reporter.GetReportByID(fingerprint.DiscoverFingerPrintScanID) + if report == nil || !report.HasData() { + return + } + + data, ok := report.Data.(fingerprint.FingerPrintData) + if !ok { + return + } + + rows := [][]string{} + for _, fp := range data.AuthServices { + rows = append(rows, []string{"Authentication Service", fp.Name}) + } + + for _, fp := range data.CDNs { + rows = append(rows, []string{"CDN", fp.Name}) + } + + for _, fp := range data.Caching { + rows = append(rows, []string{"Caching", fp.Name}) + } + + for _, fp := range data.CertificateAuthority { + rows = append(rows, []string{"Certificate Authority", fp.Name}) + } + + for _, fp := range data.Databases { + rows = append(rows, []string{"Database", fp.Name}) + } + + for _, fp := range data.Frameworks { + rows = append(rows, []string{"Framework", fp.Name}) + } + + for _, fp := range data.Hosting { + rows = append(rows, []string{"Hosting", fp.Name}) + } + + for _, fp := range data.Languages { + rows = append(rows, []string{"Language", fp.Name}) + } + + for _, fp := range data.OS { + rows = append(rows, []string{"Operating System", fp.Name}) + } + + for _, fp := range data.SecurityServices { + rows = append(rows, []string{"Security Service", fp.Name}) + } + + for _, fp := range data.ServerExtensions { + rows = append(rows, []string{"Server Extension", fp.Name}) + } + + for _, fp := range data.Servers { + rows = append(rows, []string{"Server", fp.Name}) + } + + if len(rows) == 0 { + return + } + + fmt.Println() + headers := []string{"Technologie/Service", "Value"} + table := CreateTable(headers) + + tableColors := make([]tablewriter.Colors, len(headers)) + tableColors[0] = tablewriter.Colors{tablewriter.Bold} + tableColors[1] = tablewriter.Colors{tablewriter.Bold} + + for _, row := range rows { + table.Rich(row, tableColors) + } + + table.Render() + fmt.Println() +} diff --git a/internal/cmd/printtable/output_table.go b/internal/cmd/printtable/output_table.go deleted file mode 100644 index e7a78c8..0000000 --- a/internal/cmd/printtable/output_table.go +++ /dev/null @@ -1,254 +0,0 @@ -package printtable - -import ( - "fmt" - "net/url" - "os" - "sort" - - "github.com/cerberauth/vulnapi/report" - discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" - discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" - "github.com/cerberauth/vulnapi/scan/discover/fingerprint" - "github.com/olekukonko/tablewriter" -) - -type ScanVulnerabilityReport struct { - OperationMethod string `json:"method"` - OperationPath string `json:"path"` - - Vuln *report.VulnerabilityReport `json:"vuln"` -} - -type SortByPathAndSeverity []*ScanVulnerabilityReport - -func (a SortByPathAndSeverity) Len() int { return len(a) } -func (a SortByPathAndSeverity) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a SortByPathAndSeverity) Less(i, j int) bool { - if a[i].OperationPath == a[j].OperationPath { - if a[i].OperationMethod == a[j].OperationMethod { - return a[i].Vuln.Issue.CVSS.Score > a[j].Vuln.Issue.CVSS.Score - } - - return a[i].OperationMethod < a[j].OperationMethod - } - - return a[i].OperationPath < a[j].OperationPath -} - -func NewScanVulnerabilityReports(report *report.Report) []*ScanVulnerabilityReport { - reports := report.GetFailedVulnerabilityReports() - vulns := make([]*ScanVulnerabilityReport, 0, len(reports)) - for _, vr := range reports { - url, urlErr := url.Parse(report.Operation.URL) - if urlErr != nil { - continue - } - - vulns = append(vulns, &ScanVulnerabilityReport{ - OperationMethod: report.Operation.Method, - OperationPath: url.Path, - - Vuln: vr, - }) - } - - return vulns -} - -func NewFullScanVulnerabilityReports(reports []*report.Report) []*ScanVulnerabilityReport { - vulns := make([]*ScanVulnerabilityReport, 0) - for _, r := range reports { - vulns = append(vulns, NewScanVulnerabilityReports(r)...) - } - - sort.Sort(SortByPathAndSeverity(vulns)) - - return vulns -} - -func severityTableColor(v *report.VulnerabilityReport) int { - if v.IsLowRiskSeverity() || v.IsInfoRiskSeverity() { - return tablewriter.BgBlueColor - } else if v.IsMediumRiskSeverity() { - return tablewriter.BgYellowColor - } else if v.IsHighRiskSeverity() { - return tablewriter.BgRedColor - } else if v.IsCriticalRiskSeverity() { - return tablewriter.BgHiRedColor - } - - return tablewriter.BgWhiteColor -} - -func WellKnownPathsScanReport(reporter *report.Reporter) { - openapiURL := "" - openapiReport := reporter.GetReportByID(discoverableopenapi.DiscoverableOpenAPIScanID) - if openapiReport != nil && openapiReport.HasData() { - openapiData, ok := openapiReport.Data.(discoverableopenapi.DiscoverableOpenAPIData) - if ok { - openapiURL = openapiData.URL - } - } - - graphqlURL := "" - graphqlReport := reporter.GetReportByID(discoverablegraphql.DiscoverableGraphQLPathScanID) - if graphqlReport != nil && graphqlReport.HasData() { - graphqlData, ok := graphqlReport.Data.(discoverablegraphql.DiscoverableGraphQLPathData) - if ok { - graphqlURL = graphqlData.URL - } - } - - if openapiURL == "" && graphqlURL == "" { - fmt.Println("No well-known paths were found.") - return - } - - fmt.Println() - headers := []string{"Well-Known Paths", "URL"} - table := CreateTable(headers) - - tableColors := make([]tablewriter.Colors, len(headers)) - tableColors[0] = tablewriter.Colors{tablewriter.Bold} - tableColors[1] = tablewriter.Colors{tablewriter.Bold} - - if openapiURL != "" { - table.Rich([]string{"OpenAPI", openapiURL}, tableColors) - } - if graphqlURL != "" { - table.Rich([]string{"GraphQL", graphqlURL}, tableColors) - } - - table.Render() - fmt.Println() -} - -func ContextualScanReport(reporter *report.Reporter) { - report := reporter.GetReportByID(fingerprint.DiscoverFingerPrintScanID) - if report == nil || !report.HasData() { - return - } - - data, ok := report.Data.(fingerprint.FingerPrintData) - if !ok { - return - } - - fmt.Println() - headers := []string{"Technologie/Service", "Value"} - table := CreateTable(headers) - - tableColors := make([]tablewriter.Colors, len(headers)) - tableColors[0] = tablewriter.Colors{tablewriter.Bold} - tableColors[1] = tablewriter.Colors{tablewriter.Bold} - - for _, fp := range data.AuthServices { - table.Rich([]string{"Authentication Service", fp.Name}, tableColors) - } - - for _, fp := range data.CDNs { - table.Rich([]string{"CDN", fp.Name}, tableColors) - } - - for _, fp := range data.Caching { - table.Rich([]string{"Caching", fp.Name}, tableColors) - } - - for _, fp := range data.CertificateAuthority { - table.Rich([]string{"Certificate Authority", fp.Name}, tableColors) - } - - for _, fp := range data.Databases { - table.Rich([]string{"Database", fp.Name}, tableColors) - } - - for _, fp := range data.Frameworks { - table.Rich([]string{"Framework", fp.Name}, tableColors) - } - - for _, fp := range data.Hosting { - table.Rich([]string{"Hosting", fp.Name}, tableColors) - } - - for _, fp := range data.Languages { - table.Rich([]string{"Language", fp.Name}, tableColors) - } - - for _, fp := range data.OS { - table.Rich([]string{"Operating System", fp.Name}, tableColors) - } - - for _, fp := range data.SecurityServices { - table.Rich([]string{"Security Service", fp.Name}, tableColors) - } - - for _, fp := range data.ServerExtensions { - table.Rich([]string{"Server Extension", fp.Name}, tableColors) - } - - for _, fp := range data.Servers { - table.Rich([]string{"Server", fp.Name}, tableColors) - } - - table.Render() - fmt.Println() -} - -func DisplayReportTable(reporter *report.Reporter) { - if reporter == nil || len(reporter.GetReports()) == 0 { - return - } - - headers := []string{"Operation", "Risk Level", "CVSS 4.0 Score", "OWASP", "Vulnerability"} - table := CreateTable(headers) - - vulnerabilityReports := NewFullScanVulnerabilityReports(reporter.GetReports()) - for _, vulnReport := range vulnerabilityReports { - row := []string{ - fmt.Sprintf("%s %s", vulnReport.OperationMethod, vulnReport.OperationPath), - vulnReport.Vuln.SeverityLevelString(), - fmt.Sprintf("%.1f", vulnReport.Vuln.Issue.CVSS.Score), - string(vulnReport.Vuln.Classifications.OWASP), - vulnReport.Vuln.Name, - } - - tableColors := make([]tablewriter.Colors, len(headers)) - for i := range tableColors { - if i == 1 { - tableColors[i] = tablewriter.Colors{tablewriter.Bold, severityTableColor(vulnReport.Vuln)} - } else { - tableColors[i] = tablewriter.Colors{} - } - } - - table.Rich(row, tableColors) - } - - errors := reporter.GetErrors() - if len(errors) > 0 { - fmt.Println() - fmt.Println("Errors:") - for _, err := range errors { - fmt.Printf(" - %s\n", err) - } - } - - table.Render() - fmt.Println() -} - -func CreateTable(headers []string) *tablewriter.Table { - table := tablewriter.NewWriter(os.Stdout) - table.SetHeader(headers) - table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) - table.SetCenterSeparator("|") - table.SetAutoMergeCellsByColumnIndex([]int{0}) - - return table -} - -func DisplayUnexpectedErrorMessage() { - fmt.Println() - fmt.Println("If you think that report is not accurate or if you have any suggestions for improvements, please open an issue at: https://github.com/cerberauth/vulnapi/issues/new.") -} diff --git a/internal/cmd/printtable/printttable.go b/internal/cmd/printtable/printttable.go new file mode 100644 index 0000000..285d0d7 --- /dev/null +++ b/internal/cmd/printtable/printttable.go @@ -0,0 +1,23 @@ +package printtable + +import ( + "fmt" + "os" + + "github.com/olekukonko/tablewriter" +) + +func CreateTable(headers []string) *tablewriter.Table { + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader(headers) + table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) + table.SetCenterSeparator("|") + table.SetAutoMergeCellsByColumnIndex([]int{0}) + + return table +} + +func DisplayUnexpectedErrorMessage() { + fmt.Println() + fmt.Println("If you think that report is not accurate or if you have any suggestions for improvements, please open an issue at: https://github.com/cerberauth/vulnapi/issues/new.") +} diff --git a/internal/cmd/printtable/report_table.go b/internal/cmd/printtable/report_table.go new file mode 100644 index 0000000..9a2f6cf --- /dev/null +++ b/internal/cmd/printtable/report_table.go @@ -0,0 +1,149 @@ +package printtable + +import ( + "fmt" + "net/url" + "sort" + + "github.com/cerberauth/vulnapi/report" + "github.com/olekukonko/tablewriter" +) + +type ScanVulnerabilityReport struct { + OperationMethod string `json:"method"` + OperationPath string `json:"path"` + + Vuln *report.VulnerabilityReport `json:"vuln"` +} + +type SortByPathAndSeverity []*ScanVulnerabilityReport + +func (a SortByPathAndSeverity) Len() int { return len(a) } +func (a SortByPathAndSeverity) Swap(i, j int) { a[i], a[j] = a[j], a[i] } +func (a SortByPathAndSeverity) Less(i, j int) bool { + if a[i].OperationPath == a[j].OperationPath { + if a[i].OperationMethod == a[j].OperationMethod { + return a[i].Vuln.Issue.CVSS.Score > a[j].Vuln.Issue.CVSS.Score + } + + return a[i].OperationMethod < a[j].OperationMethod + } + + return a[i].OperationPath < a[j].OperationPath +} + +func NewScanVulnerabilityReports(r *report.Report) []*ScanVulnerabilityReport { + vulnReports := r.GetFailedVulnerabilityReports() + vulns := make([]*ScanVulnerabilityReport, 0, len(vulnReports)) + for _, vulnReport := range vulnReports { + url, urlErr := url.Parse(r.Operation.URL) + if urlErr != nil { + continue + } + + vulns = append(vulns, &ScanVulnerabilityReport{ + OperationMethod: r.Operation.Method, + OperationPath: url.Path, + + Vuln: vulnReport, + }) + } + + return vulns +} + +func NewFullScanVulnerabilityReports(reports []*report.Report) []*ScanVulnerabilityReport { + vulns := make([]*ScanVulnerabilityReport, 0) + for _, r := range reports { + vulns = append(vulns, NewScanVulnerabilityReports(r)...) + } + + sort.Sort(SortByPathAndSeverity(vulns)) + + return vulns +} + +func severityTableColor(v *report.VulnerabilityReport) int { + if v.IsLowRiskSeverity() || v.IsInfoRiskSeverity() { + return tablewriter.BgBlueColor + } else if v.IsMediumRiskSeverity() { + return tablewriter.BgYellowColor + } else if v.IsHighRiskSeverity() { + return tablewriter.BgRedColor + } else if v.IsCriticalRiskSeverity() { + return tablewriter.BgHiRedColor + } + + return tablewriter.BgWhiteColor +} + +func DisplayReportSummaryTable(r *report.Reporter) { + if r == nil || len(r.GetReports()) == 0 { + return + } + + fmt.Println() + headers := []string{"Status", "Scans Number"} + table := CreateTable(headers) + + tableColors := make([]tablewriter.Colors, len(headers)) + tableColors[0] = tablewriter.Colors{tablewriter.Bold} + tableColors[1] = tablewriter.Colors{tablewriter.Bold} + + for _, status := range report.VulnerabilityReportStatuses { + scansNumber := len(r.GetReportsByVulnerabilityStatus(status)) + + row := []string{ + status.String(), + fmt.Sprintf("%d", scansNumber), + } + + table.Rich(row, tableColors) + } + + table.Render() + fmt.Println() +} + +func DisplayReportTable(r *report.Reporter) { + if r == nil || !r.HasVulnerability() { + return + } + + headers := []string{"Operation", "Risk Level", "CVSS 4.0 Score", "OWASP", "Vulnerability"} + table := CreateTable(headers) + + vulnerabilityReports := NewFullScanVulnerabilityReports(r.GetReports()) + for _, vulnReport := range vulnerabilityReports { + row := []string{ + fmt.Sprintf("%s %s", vulnReport.OperationMethod, vulnReport.OperationPath), + vulnReport.Vuln.SeverityLevelString(), + fmt.Sprintf("%.1f", vulnReport.Vuln.Issue.CVSS.Score), + string(vulnReport.Vuln.Classifications.OWASP), + vulnReport.Vuln.Name, + } + + tableColors := make([]tablewriter.Colors, len(headers)) + for i := range tableColors { + if i == 1 { + tableColors[i] = tablewriter.Colors{tablewriter.Bold, severityTableColor(vulnReport.Vuln)} + } else { + tableColors[i] = tablewriter.Colors{} + } + } + + table.Rich(row, tableColors) + } + + errors := r.GetErrors() + if len(errors) > 0 { + fmt.Println() + fmt.Println("Errors:") + for _, err := range errors { + fmt.Printf(" - %s\n", err) + } + } + + table.Render() + fmt.Println() +} diff --git a/internal/cmd/printtable/output_table_test.go b/internal/cmd/printtable/report_table_test.go similarity index 100% rename from internal/cmd/printtable/output_table_test.go rename to internal/cmd/printtable/report_table_test.go diff --git a/internal/cmd/printtable/wellknown_paths_table.go b/internal/cmd/printtable/wellknown_paths_table.go new file mode 100644 index 0000000..d29be4a --- /dev/null +++ b/internal/cmd/printtable/wellknown_paths_table.go @@ -0,0 +1,53 @@ +package printtable + +import ( + "fmt" + + "github.com/cerberauth/vulnapi/report" + discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" + discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" + "github.com/olekukonko/tablewriter" +) + +func WellKnownPathsScanReport(reporter *report.Reporter) { + openapiURL := "" + openapiReport := reporter.GetReportByID(discoverableopenapi.DiscoverableOpenAPIScanID) + if openapiReport != nil && openapiReport.HasData() { + openapiData, ok := openapiReport.Data.(discoverableopenapi.DiscoverableOpenAPIData) + if ok { + openapiURL = openapiData.URL + } + } + + graphqlURL := "" + graphqlReport := reporter.GetReportByID(discoverablegraphql.DiscoverableGraphQLPathScanID) + if graphqlReport != nil && graphqlReport.HasData() { + graphqlData, ok := graphqlReport.Data.(discoverablegraphql.DiscoverableGraphQLPathData) + if ok { + graphqlURL = graphqlData.URL + } + } + + if openapiURL == "" && graphqlURL == "" { + fmt.Println("No well-known paths were found.") + return + } + + fmt.Println() + headers := []string{"Well-Known Paths", "URL"} + table := CreateTable(headers) + + tableColors := make([]tablewriter.Colors, len(headers)) + tableColors[0] = tablewriter.Colors{tablewriter.Bold} + tableColors[1] = tablewriter.Colors{tablewriter.Bold} + + if openapiURL != "" { + table.Rich([]string{"OpenAPI", openapiURL}, tableColors) + } + if graphqlURL != "" { + table.Rich([]string{"GraphQL", graphqlURL}, tableColors) + } + + table.Render() + fmt.Println() +} diff --git a/report/reporter.go b/report/reporter.go index 19318d6..58358b0 100644 --- a/report/reporter.go +++ b/report/reporter.go @@ -28,6 +28,20 @@ func (rr *Reporter) GetReportByID(id string) *Report { return nil } +func (rr *Reporter) GetReportsByVulnerabilityStatus(status VulnerabilityReportStatus) []*Report { + var reports []*Report + for _, r := range rr.GetReports() { + for _, vr := range r.GetVulnerabilityReports() { + if vr.Status == status { + reports = append(reports, r) + break + } + } + } + + return reports +} + func (rr *Reporter) GetErrors() []error { var errors []error for _, r := range rr.GetReports() { diff --git a/report/reporter_test.go b/report/reporter_test.go index 1945cf9..d3931c9 100644 --- a/report/reporter_test.go +++ b/report/reporter_test.go @@ -148,3 +148,67 @@ func TestReporter_HasHigherThanSeverityThresholdVulnerability_WhenAboveThreshold assert.True(t, reporter.HasHigherThanSeverityThresholdVulnerability(5.0)) } + +func TestReporter_GetReportsByVulnerabilityStatus_NoReports(t *testing.T) { + reporter := report.NewReporter() + reports := reporter.GetReportsByVulnerabilityStatus(report.VulnerabilityReportStatusFailed) + assert.Empty(t, reports) +} + +func TestReporter_GetReportsByVulnerabilityStatus_NoMatchingStatus(t *testing.T) { + reporter := report.NewReporter() + operation, _ := request.NewOperation(http.MethodPost, "http://localhost:8080/", nil, nil) + sr := report.NewScanReport("id", "test", operation) + issue := report.Issue{ + Name: "test", + } + vulnerabilityReport := report.NewVulnerabilityReport(issue).Pass() + sr.AddVulnerabilityReport(vulnerabilityReport) + reporter.AddReport(sr) + + reports := reporter.GetReportsByVulnerabilityStatus(report.VulnerabilityReportStatusFailed) + assert.Empty(t, reports) +} + +func TestReporter_GetReportsByVulnerabilityStatus_MatchingStatus(t *testing.T) { + reporter := report.NewReporter() + operation, _ := request.NewOperation(http.MethodPost, "http://localhost:8080/", nil, nil) + sr := report.NewScanReport("id", "test", operation) + issue := report.Issue{ + Name: "test", + } + vulnerabilityReport := report.NewVulnerabilityReport(issue).Fail() + sr.AddVulnerabilityReport(vulnerabilityReport) + reporter.AddReport(sr) + + reports := reporter.GetReportsByVulnerabilityStatus(report.VulnerabilityReportStatusFailed) + assert.NotEmpty(t, reports) + assert.Equal(t, 1, len(reports)) + assert.Equal(t, "id", reports[0].ID) +} + +func TestReporter_GetReportsByVulnerabilityStatus_MultipleReports(t *testing.T) { + reporter := report.NewReporter() + operation, _ := request.NewOperation(http.MethodPost, "http://localhost:8080/", nil, nil) + sr1 := report.NewScanReport("id1", "test1", operation) + issue1 := report.Issue{ + Name: "test1", + } + vulnerabilityReport1 := report.NewVulnerabilityReport(issue1).Fail() + sr1.AddVulnerabilityReport(vulnerabilityReport1) + reporter.AddReport(sr1) + + sr2 := report.NewScanReport("id2", "test2", operation) + issue2 := report.Issue{ + Name: "test2", + } + vulnerabilityReport2 := report.NewVulnerabilityReport(issue2).Fail() + sr2.AddVulnerabilityReport(vulnerabilityReport2) + reporter.AddReport(sr2) + + reports := reporter.GetReportsByVulnerabilityStatus(report.VulnerabilityReportStatusFailed) + assert.NotEmpty(t, reports) + assert.Equal(t, 2, len(reports)) + assert.Equal(t, "id1", reports[0].ID) + assert.Equal(t, "id2", reports[1].ID) +} diff --git a/report/vuln.go b/report/vuln.go index 839f035..aa20ace 100644 --- a/report/vuln.go +++ b/report/vuln.go @@ -9,6 +9,10 @@ import ( type VulnerabilityReportStatus string +func (vrs VulnerabilityReportStatus) String() string { + return string(vrs) +} + const ( VulnerabilityReportStatusPassed VulnerabilityReportStatus = "passed" VulnerabilityReportStatusFailed VulnerabilityReportStatus = "failed" @@ -16,6 +20,13 @@ const ( VulnerabilityReportStatusNone VulnerabilityReportStatus = "none" ) +var VulnerabilityReportStatuses = []VulnerabilityReportStatus{ + VulnerabilityReportStatusPassed, + VulnerabilityReportStatusFailed, + VulnerabilityReportStatusSkipped, + VulnerabilityReportStatusNone, +} + type VulnerabilityReport struct { Issue `json:",inline" yaml:",inline"` Status VulnerabilityReportStatus `json:"status" yaml:"status"`