Skip to content

Commit f4d2407

Browse files
feat: add table rendering output for openapi scan
1 parent 42f1cbe commit f4d2407

17 files changed

+189
-141
lines changed

README.md

+23-4
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,29 @@ echo "eyJhbGciOiJub25lIiwidHlwIjoiSldUIn0.e30." | vulnapi scan openapi ./test/st
5151
The CLI provides detailed reports on any vulnerabilities detected during the scan. Below is an example of the output format:
5252

5353
```bash
54-
2024/02/12 16:09:30 [critical][JWT Alg None] http://localhost:8080/: JWT accepts none algorithm and does verify jwt.
55-
2024/02/12 16:09:30 [critical][JWT Alg None] http://localhost:8080/: JWT accepts none algorithm and does verify jwt.
56-
2024/02/12 16:09:30 [critical][JWT Alg None] http://localhost:8080/resources/ours: JWT accepts none algorithm and does verify jwt.
57-
2024/02/12 16:09:30 [critical][JWT Alg None] http://localhost:8080/resources/those: JWT accepts none algorithm and does verify jwt.
54+
+------------+--------------------------------+--------------------------------+----------------------------+
55+
| RISK LEVEL | VULNERABILITY | DESCRIPTION | OPERATION |
56+
+------------+--------------------------------+--------------------------------+----------------------------+
57+
| Critical | JWT None Algorithm | JWT with none algorithm is | GET http://localhost:8080/ |
58+
| | | accepted allowing to bypass | |
59+
| | | authentication. | |
60+
| Low | CSP Header is not set | No Content Security Policy | GET http://localhost:8080/ |
61+
| | | (CSP) Header has been detected | |
62+
| | | in HTTP Response. | |
63+
| Low | CORS Header is not set | No CORS Header has been | GET http://localhost:8080/ |
64+
| | | detected in HTTP Response. | |
65+
| Low | HSTS Header is not set | No HSTS Header has been | GET http://localhost:8080/ |
66+
| | | detected in HTTP Response. | |
67+
| Low | X-Content-Type-Options Header | No X-Content-Type-Options | GET http://localhost:8080/ |
68+
| | is not set | Header has been detected in | |
69+
| | | HTTP Response. | |
70+
| Low | X-Frame-Options Header is not | No X-Frame-Options Header | GET http://localhost:8080/ |
71+
| | set | has been detected in HTTP | |
72+
| | | Response. | |
73+
| Low | HTTP Trace Method enabled | HTTP Trace method seems | GET http://localhost:8080/ |
74+
| | | enabled for this request. | |
75+
+------------+--------------------------------+--------------------------------+----------------------------+
76+
Warning: Critical vulnerabilities detected!
5877
```
5978

6079
In this example, each line represents a detected vulnerability, including the timestamp, severity level (critical), vulnerability type (JWT Alg None), affected endpoint (http://localhost:8080/), and a description of the vulnerability (JWT accepts none algorithm and does not verify JWT).

cmd/scan/curl.go

+1-51
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,9 @@ package scan
33
import (
44
"log"
55
"net/http"
6-
"os"
76
"strings"
87

98
"github.com/cerberauth/vulnapi/scan"
10-
"github.com/common-nighthawk/go-figure"
11-
"github.com/fatih/color"
12-
"github.com/olekukonko/tablewriter"
139
"github.com/spf13/cobra"
1410
)
1511

@@ -28,9 +24,6 @@ func NewCURLScanCmd() (scanCmd *cobra.Command) {
2824
FParseErrWhitelist: cobra.FParseErrWhitelist{
2925
UnknownFlags: true,
3026
},
31-
PreRun: func(cmd *cobra.Command, args []string) {
32-
figure.NewColorFigure("VulnAPI", "", "cyan", true).Print()
33-
},
3427
Run: func(cmd *cobra.Command, args []string) {
3528
url = args[0]
3629

@@ -54,52 +47,9 @@ func NewCURLScanCmd() (scanCmd *cobra.Command) {
5447
log.Fatal(err)
5548
}
5649

57-
reporter, _, err := scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute()
58-
if err != nil {
50+
if reporter, _, err = scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute(); err != nil {
5951
log.Fatal(err)
6052
}
61-
62-
var outputColor *color.Color
63-
var outputMessage string
64-
var outputStream *os.File
65-
if !reporter.HasVulnerability() {
66-
outputColor = color.New(color.FgGreen)
67-
outputMessage = "Congratulations! No issues were found."
68-
outputStream = os.Stdout
69-
} else if reporter.HasHighRiskSeverityVulnerability() {
70-
outputColor = color.New(color.FgRed)
71-
outputMessage = "Warning: Critical vulnerabilities detected!"
72-
outputStream = os.Stderr
73-
} else {
74-
outputColor = color.New(color.FgYellow)
75-
outputMessage = "Advice: There are some low-risk issues. It's advised to take a look."
76-
outputStream = os.Stderr
77-
}
78-
79-
table := tablewriter.NewWriter(outputStream)
80-
table.SetHeader([]string{"Risk Level", "Vulnerability", "Description"})
81-
82-
for _, v := range reporter.GetVulnerabilityReports() {
83-
var lineColor int
84-
if v.IsLowRiskSeverity() {
85-
lineColor = tablewriter.BgBlueColor
86-
} else if v.IsMediumRiskSeverity() {
87-
lineColor = tablewriter.FgYellowColor
88-
} else if v.IsHighRiskSeverity() {
89-
lineColor = tablewriter.FgRedColor
90-
}
91-
92-
table.Rich(
93-
[]string{v.SeverityLevelString(), v.Name, v.Description},
94-
[]tablewriter.Colors{
95-
{tablewriter.Bold, lineColor},
96-
{tablewriter.Normal, lineColor},
97-
{tablewriter.Normal, tablewriter.FgWhiteColor}},
98-
)
99-
}
100-
101-
table.Render()
102-
outputColor.Fprintln(outputStream, outputMessage)
10353
},
10454
}
10555

cmd/scan/openapi.go

+1-12
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,9 @@ func NewOpenAPIScanCmd() (scanCmd *cobra.Command) {
4343
log.Fatal(err)
4444
}
4545

46-
rpr, _, err := scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute()
47-
if err != nil {
46+
if reporter, _, err = scan.WithAllVulnsScans().WithAllBestPracticesScans().Execute(); err != nil {
4847
log.Fatal(err)
4948
}
50-
51-
for _, r := range rpr.GetVulnerabilityReports() {
52-
log.Println(r)
53-
}
54-
55-
if !rpr.HasVulnerability() {
56-
log.Println("Congratulations! No vulnerability has been discovered!")
57-
} else {
58-
log.Fatalln("There is one or more vulnerabilies you should know.")
59-
}
6049
},
6150
}
6251

cmd/scan/root.go

+72
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,85 @@
11
package scan
22

33
import (
4+
"fmt"
5+
"os"
6+
7+
"github.com/cerberauth/vulnapi/report"
8+
"github.com/common-nighthawk/go-figure"
9+
"github.com/fatih/color"
10+
"github.com/olekukonko/tablewriter"
411
"github.com/spf13/cobra"
512
)
613

14+
var reporter *report.Reporter
15+
16+
func severityTableColor(v *report.VulnerabilityReport) int {
17+
if v.IsLowRiskSeverity() {
18+
return tablewriter.BgBlueColor
19+
} else if v.IsMediumRiskSeverity() {
20+
return tablewriter.FgYellowColor
21+
} else if v.IsHighRiskSeverity() {
22+
return tablewriter.FgRedColor
23+
}
24+
25+
return tablewriter.BgWhiteColor
26+
}
27+
728
func NewScanCmd() (scanCmd *cobra.Command) {
829
scanCmd = &cobra.Command{
930
Use: "scan [type]",
1031
Short: "API Scan",
32+
PersistentPreRun: func(cmd *cobra.Command, args []string) {
33+
figure.NewColorFigure("VulnAPI", "", "cyan", true).Print()
34+
},
35+
PersistentPostRun: func(cmd *cobra.Command, args []string) {
36+
if reporter == nil {
37+
return
38+
}
39+
40+
var outputColor *color.Color
41+
var outputMessage string
42+
var outputStream *os.File
43+
if !reporter.HasVulnerability() {
44+
outputColor = color.New(color.FgGreen)
45+
outputMessage = "Congratulations! No issues were found."
46+
outputStream = os.Stdout
47+
} else if reporter.HasHighRiskSeverityVulnerability() {
48+
outputColor = color.New(color.FgRed)
49+
outputMessage = "Warning: Critical vulnerabilities detected!"
50+
outputStream = os.Stderr
51+
} else {
52+
outputColor = color.New(color.FgYellow)
53+
outputMessage = "Advice: There are some low-risk issues. It's advised to take a look."
54+
outputStream = os.Stderr
55+
}
56+
57+
headers := []string{"Risk Level", "Vulnerability", "Description"}
58+
if cmd.Name() == "openapi" {
59+
headers = append(headers, "Operation")
60+
}
61+
62+
table := tablewriter.NewWriter(outputStream)
63+
table.SetHeader(headers)
64+
65+
for _, v := range reporter.GetVulnerabilityReports() {
66+
lineColor := severityTableColor(v)
67+
row := []string{v.SeverityLevelString(), v.Name, v.Description}
68+
if cmd.Name() == "openapi" {
69+
row = append(row, fmt.Sprintf("%s %s", v.Operation.Method, v.Operation.Url))
70+
}
71+
72+
tableColors := make([]tablewriter.Colors, len(headers))
73+
for i := range tableColors {
74+
tableColors[i] = tablewriter.Colors{tablewriter.Bold, lineColor}
75+
}
76+
77+
table.Rich(row, tableColors)
78+
}
79+
80+
table.Render()
81+
outputColor.Fprintln(outputStream, outputMessage)
82+
},
1183
}
1284
scanCmd.AddCommand(NewCURLScanCmd())
1385
scanCmd.AddCommand(NewOpenAPIScanCmd())

internal/request/operation.go

+6-4
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import (
66
"github.com/cerberauth/vulnapi/internal/auth"
77
)
88

9-
type Operations []Operation
9+
type Operations []*Operation
1010

1111
func (o Operations) Len() int { return len(o) }
1212
func (o Operations) Swap(i, j int) { o[i], o[j] = o[j], o[i] }
@@ -27,21 +27,23 @@ type Operation struct {
2727
SecuritySchemes []auth.SecurityScheme
2828
}
2929

30-
func NewOperation(url, method string, headers *http.Header, cookies []http.Cookie, securitySchemes []auth.SecurityScheme) Operation {
30+
func NewOperation(url, method string, headers *http.Header, cookies []http.Cookie, securitySchemes []auth.SecurityScheme) *Operation {
3131
if len(securitySchemes) == 0 {
3232
securitySchemes = []auth.SecurityScheme{auth.NewNoAuthSecurityScheme()}
3333
}
3434

35-
return Operation{
35+
operation := Operation{
3636
Url: url,
3737
Method: method,
3838
Headers: headers,
3939
Cookies: cookies,
4040
SecuritySchemes: securitySchemes,
4141
}
42+
43+
return &operation
4244
}
4345

44-
func (o Operation) Clone() Operation {
46+
func (o *Operation) Clone() *Operation {
4547
clonedHeaders := make(http.Header)
4648
if o.Headers != nil {
4749
clonedHeaders = o.Headers.Clone()

internal/request/scan_url.go internal/scan/scan_url.go

+4-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
1-
package request
1+
package scan
22

33
import (
44
"fmt"
55

66
"github.com/cerberauth/vulnapi/internal/auth"
7+
"github.com/cerberauth/vulnapi/internal/request"
78
"github.com/cerberauth/vulnapi/report"
89
)
910

10-
func ScanURL(operation *Operation, securityScheme *auth.SecurityScheme) (*report.VulnerabilityScanAttempt, error) {
11-
req, err := NewRequest(operation.Method, operation.Url, nil)
11+
func ScanURL(operation *request.Operation, securityScheme *auth.SecurityScheme) (*report.VulnerabilityScanAttempt, error) {
12+
req, err := request.NewRequest(operation.Method, operation.Url, nil)
1213
if err != nil {
1314
return nil, fmt.Errorf("request with url %s has an unexpected error", err)
1415
} else {

report/vuln.go

+5-2
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,16 @@ package report
22

33
import (
44
"fmt"
5+
6+
"github.com/cerberauth/vulnapi/internal/request"
57
)
68

79
type VulnerabilityReport struct {
810
SeverityLevel float64 // https://nvd.nist.gov/vuln-metrics/cvss
911
Name string
1012
Description string
11-
Url string
13+
14+
Operation *request.Operation
1215
}
1316

1417
func (vr *VulnerabilityReport) IsLowRiskSeverity() bool {
@@ -24,7 +27,7 @@ func (vr *VulnerabilityReport) IsHighRiskSeverity() bool {
2427
}
2528

2629
func (vr *VulnerabilityReport) String() string {
27-
return fmt.Sprintf("[%s][%s] %s: %s", vr.SeverityLevelString(), vr.Name, vr.Url, vr.Description)
30+
return fmt.Sprintf("[%s][%s] %s %s: %s", vr.SeverityLevelString(), vr.Name, vr.Operation.Method, vr.Operation.Url, vr.Description)
2831
}
2932

3033
func (vr *VulnerabilityReport) SeverityLevelString() string {

report/vuln_test.go

+7-2
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package report_test
33
import (
44
"testing"
55

6+
"github.com/cerberauth/vulnapi/internal/request"
67
"github.com/cerberauth/vulnapi/report"
78
"github.com/stretchr/testify/assert"
89
)
@@ -27,9 +28,13 @@ func TestVulnerabilityReport_String(t *testing.T) {
2728
SeverityLevel: 7.5,
2829
Name: "Test Vulnerability",
2930
Description: "This is a test vulnerability",
30-
Url: "https://example.com/vulnerability",
31+
32+
Operation: &request.Operation{
33+
Method: "GET",
34+
Url: "https://example.com/vulnerability",
35+
},
3136
}
32-
expected := "[High][Test Vulnerability] https://example.com/vulnerability: This is a test vulnerability"
37+
expected := "[High][Test Vulnerability] GET https://example.com/vulnerability: This is a test vulnerability"
3338
assert.Equal(t, expected, vr.String())
3439
}
3540
func TestVulnerabilityReport_SeverityLevelString(t *testing.T) {

0 commit comments

Comments
 (0)