Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add scan reporter #20

Merged
merged 1 commit into from
Oct 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions cmd/scan/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,17 +32,17 @@ func NewScanCmd() (scanCmd *cobra.Command) {
jwt = stdin
}

reports, err := scan.NewScanner(url, &jwt).WithAllScans().Execute()
rpr, _, err := scan.NewScanner(url, &jwt).WithAllScans().Execute()
if err != nil {
log.Fatal(err)
}

if len(reports) == 0 {
if !rpr.HasVulnerability() {
println("Congratulations! No vulnerability has been discovered!")
}

for _, report := range reports {
log.Println(report)
for _, r := range rpr.GetVulnerabilityReports() {
log.Println(r)
}
},
}
Expand Down
29 changes: 16 additions & 13 deletions internal/request/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,34 @@ package request

import (
"fmt"
"io"
"net/http"
)

func SendRequestWithBearerAuth(url string, token string) (int, []byte, error) {
client := &http.Client{}

req, err := http.NewRequest("GET", url, nil)
func prepareVulnAPIRequest(method string, url string) (*http.Request, error) {
req, err := http.NewRequest(method, url, nil)
if err != nil {
return 0, nil, err
return nil, err
}

req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "vulnapi/0.1")

resp, err := client.Do(req)
return req, nil
}

func SendRequestWithBearerAuth(url string, token string) (*http.Request, *http.Response, error) {
req, err := prepareVulnAPIRequest("GET", url)
if err != nil {
return 0, nil, err
return req, nil, err
}
defer resp.Body.Close()

body, err := io.ReadAll(resp.Body)
req.Header.Add("Authorization", fmt.Sprintf("Bearer %s", token))

client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return 0, nil, err
return req, resp, err
}
defer resp.Body.Close()

return resp.StatusCode, body, nil
return req, resp, nil
}
59 changes: 59 additions & 0 deletions report/report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package report

import (
"net/http"
"time"
)

type VulnerabilityScanAttempt struct {
Request *http.Request
Response *http.Response

Err error
}

type ScanReport struct {
scans []*VulnerabilityScanAttempt
vulns []*VulnerabilityReport

startTime time.Time
endTime time.Time
}

func NewScanReport() *ScanReport {
return &ScanReport{
startTime: time.Now(),
}
}

func (sc *ScanReport) Start() *ScanReport {
sc.startTime = time.Now()
return sc
}

func (sc *ScanReport) End() *ScanReport {
sc.endTime = time.Now()
return sc
}

func (sc *ScanReport) AddScanAttempt(a *VulnerabilityScanAttempt) *ScanReport {
sc.scans = append(sc.scans, a)
return sc
}

func (sc *ScanReport) GetScanAttempts() []*VulnerabilityScanAttempt {
return sc.scans
}

func (sc *ScanReport) AddVulnerabilityReport(vr *VulnerabilityReport) *ScanReport {
sc.vulns = append(sc.vulns, vr)
return sc
}

func (sc *ScanReport) GetVulnerabilityReports() []*VulnerabilityReport {
return sc.vulns
}

func (sc *ScanReport) HasVulnerabilityReport() bool {
return len(sc.GetVulnerabilityReports()) > 0
}
48 changes: 48 additions & 0 deletions report/reporter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package report

type Reporter struct {
reports []*ScanReport
}

func NewReporter() *Reporter {
return &Reporter{
reports: []*ScanReport{},
}
}

func (rr *Reporter) AddReport(r *ScanReport) {
rr.reports = append(rr.reports, r)
}

func (rr *Reporter) GetReports() []*ScanReport {
return rr.reports
}

func (rr *Reporter) HasVulnerability() bool {
for _, r := range rr.GetReports() {
if r.HasVulnerabilityReport() {
return true
}
}

return false
}

func (rr *Reporter) GetVulnerabilityReports() []*VulnerabilityReport {
var vrs []*VulnerabilityReport
for _, r := range rr.GetReports() {
vrs = append(vrs, r.GetVulnerabilityReports()...)
}

return vrs
}

func (rr *Reporter) HasHighRiskSeverityVulnerability() bool {
for _, r := range rr.GetVulnerabilityReports() {
if r.IsHighRiskSeverity() {
return true
}
}

return false
}
40 changes: 40 additions & 0 deletions report/vuln.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package report

import "fmt"

type VulnerabilityReport struct {
SeverityLevel float64 // https://nvd.nist.gov/vuln-metrics/cvss
Name string
Description string
Url *string
}

func (vr *VulnerabilityReport) IsLowRiskSeverity() bool {
return vr.SeverityLevel < 4
}

func (vr *VulnerabilityReport) IsMediumRiskSeverity() bool {
return vr.SeverityLevel > 4 && vr.SeverityLevel < 7
}

func (vr *VulnerabilityReport) IsHighRiskSeverity() bool {
return vr.SeverityLevel > 7
}

func (vr *VulnerabilityReport) String() string {
return fmt.Sprintf("[%s] %s: %s", severyLevelString(vr.SeverityLevel), vr.Name, vr.Description)
}

func severyLevelString(severityLevel float64) string {
if severityLevel >= 9 {
return "critical"
} else if severityLevel < 9 && severityLevel >= 7 {
return "hight"
} else if severityLevel < 7 && severityLevel >= 4 {
return "medium"
} else if severityLevel < 4 && severityLevel >= 0.1 {
return "low"
} else {
return "none"
}
}
34 changes: 21 additions & 13 deletions scan/jwt/alg_none.go
Original file line number Diff line number Diff line change
@@ -1,26 +1,34 @@
package jwt

import (
"fmt"

"github.com/cerberauth/vulnapi/internal/request"
"github.com/cerberauth/vulnapi/report"
restapi "github.com/cerberauth/vulnapi/scan/rest_api"
"github.com/golang-jwt/jwt/v5"
)

func AlgNoneJwtScanHandler(url string, token string) []error {
newToken, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodNone, jwt.UnsafeAllowNoneSignatureType)
if err != nil {
return []error{err}
}
const (
AlgNoneVulnerabilitySeverityLevel = 9
AlgNoneVulnerabilityName = "JWT Alg None"
AlgNoneVulnerabilityDescription = "JWT accepts none algorithm and does verify jwt."
)

func AlgNoneJwtScanHandler(url string, token string) (*report.ScanReport, error) {
r := report.NewScanReport()

statusCode, _, err := request.SendRequestWithBearerAuth(url, newToken)
newToken, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodNone, jwt.UnsafeAllowNoneSignatureType)
if err != nil {
return []error{err}
return r, err
}
vsa := restapi.ScanRestAPI(url, newToken)
r.AddScanAttempt(vsa).End()

if statusCode > 200 && statusCode <= 300 {
return []error{fmt.Errorf("unexpected status code %d with an alg none forged token", statusCode)}
if vsa.Response.StatusCode < 300 {
r.AddVulnerabilityReport(&report.VulnerabilityReport{
SeverityLevel: AlgNoneVulnerabilitySeverityLevel,
Name: AlgNoneVulnerabilityName,
Description: AlgNoneVulnerabilityDescription,
})
}

return nil
return r, nil
}
51 changes: 25 additions & 26 deletions scan/jwt/not_verified.go
Original file line number Diff line number Diff line change
@@ -1,46 +1,45 @@
package jwt

import (
"fmt"

"github.com/cerberauth/vulnapi/internal/request"
"github.com/cerberauth/vulnapi/report"
restapi "github.com/cerberauth/vulnapi/scan/rest_api"
"github.com/golang-jwt/jwt/v5"
)

func NotVerifiedScanHandler(url string, token string) []error {
const (
NotVerifiedVulnerabilitySeverityLevel = 9
NotVerifiedVulnerabilityName = "JWT Not Verified"
NotVerifiedVulnerabilityDescription = "JWT is not verified."
)

func NotVerifiedScanHandler(url string, token string) (*report.ScanReport, error) {
r := report.NewScanReport()

newTokenA, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodHS256, []byte("a"))
if err != nil {
return []error{err}
return r, err
}

newTokenB, err := createNewJWTWithClaimsAndMethod(token, jwt.SigningMethodHS256, []byte("b"))
if err != nil {
return []error{err}
}

statusCodeA, _, errRequestA := request.SendRequestWithBearerAuth(url, newTokenA)
statusCodeB, _, errRequestB := request.SendRequestWithBearerAuth(url, newTokenB)

var errors []error
if errRequestA != nil {
errors = append(errors, errRequestA)
return r, err
}

if errRequestB != nil {
errors = append(errors, errRequestB)
}
vsa1 := restapi.ScanRestAPI(url, newTokenA)
r.AddScanAttempt(vsa1)

if statusCodeA > 200 && statusCodeA <= 300 {
errors = append(errors, fmt.Errorf("unexpected status code %d with an invalid forged token", statusCodeA))
}
vsa2 := restapi.ScanRestAPI(url, newTokenB)
r.AddScanAttempt(vsa2)

if statusCodeA != statusCodeB {
errors = append(errors, fmt.Errorf("status code are not the same between the two attempts"))
}
r.End()

if len(errors) > 0 {
return errors
if vsa1.Response.StatusCode != vsa2.Response.StatusCode {
r.AddVulnerabilityReport(&report.VulnerabilityReport{
SeverityLevel: NotVerifiedVulnerabilitySeverityLevel,
Name: NotVerifiedVulnerabilityName,
Description: NotVerifiedVulnerabilityDescription,
})
}

return nil
return r, nil
}
35 changes: 22 additions & 13 deletions scan/jwt/null_signature.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
package jwt

import (
"fmt"
"strings"

"github.com/cerberauth/vulnapi/internal/request"
"github.com/cerberauth/vulnapi/report"
restapi "github.com/cerberauth/vulnapi/scan/rest_api"
)

const (
NullSigVulnerabilitySeverityLevel = 9
NullSigVulnerabilityName = "JWT Null Signature"
NullSigVulnerabilityDescription = "JWT with null signature is accepted allowing to bypass authentication."
)

func createNewJWTWithoutSignature(originalTokenString string) (string, error) {
Expand All @@ -17,20 +23,23 @@ func createNewJWTWithoutSignature(originalTokenString string) (string, error) {
return strings.Join([]string{parts[0], parts[1], ""}, "."), nil
}

func NullSignatureScanHandler(url string, token string) []error {
newToken, err := createNewJWTWithoutSignature(token)
if err != nil {
return []error{err}
}
func NullSignatureScanHandler(url string, token string) (*report.ScanReport, error) {
r := report.NewScanReport()

statusCode, _, err := request.SendRequestWithBearerAuth(url, newToken)
newToken, err := createNewJWTWithoutSignature(token)
if err != nil {
return []error{err}
return r, err
}

if statusCode > 200 && statusCode <= 300 {
return []error{fmt.Errorf("unexpected status code %d with a null signature", statusCode)}
vsa := restapi.ScanRestAPI(url, newToken)
r.AddScanAttempt(vsa).End()

if vsa.Response.StatusCode < 300 {
r.AddVulnerabilityReport(&report.VulnerabilityReport{
SeverityLevel: NullSigVulnerabilitySeverityLevel,
Name: NullSigVulnerabilityName,
Description: NullSigVulnerabilityDescription,
})
}

return nil
return r, nil
}
Loading