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

Add GraphQL as separate protocol #250

Merged
merged 2 commits into from
Jul 26, 2024
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
18 changes: 11 additions & 7 deletions cmd/gotestwaf/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package main

import (
"fmt"
"net/url"
"os"
"path/filepath"
"regexp"
Expand Down Expand Up @@ -76,6 +75,7 @@ func parseFlags() (args []string, err error) {
// Target settings
urlParam := flag.String("url", "", "URL to check")
flag.Uint16("grpcPort", 0, "gRPC port to check")
graphqlURL := flag.String("graphqlURL", "", "GraphQL URL to check")
openapiFile := flag.String("openapiFile", "", "Path to openAPI file")

// Test cases settings
Expand Down Expand Up @@ -170,15 +170,19 @@ func parseFlags() (args []string, err error) {
return nil, fmt.Errorf("unknown logging format: %s", logFormat)
}

validURL, err := url.Parse(*urlParam)
if err != nil ||
(validURL.Scheme != "http" && validURL.Scheme != "https") ||
validURL.Host == "" {
return nil, errors.New("URL is not valid")
validURL, err := validateURL(*urlParam, httpProto)
if err != nil {
return nil, errors.Wrap(err, "URL is not valid")
}

*urlParam = validURL.String()

// format GraphQL URL from given HTTP URL
gqlValidURL, err := checkOrCraftProtocolURL(*graphqlURL, *urlParam, graphqlProto)
if err != nil {
return nil, errors.Wrap(err, "graphqlURL is not valid")
}
*graphqlURL = gqlValidURL.String()

// Force GoHTTP to be used as the HTTP client
// when scanning against the OpenAPI spec.
if openapiFile != nil && len(*openapiFile) > 0 {
Expand Down
67 changes: 67 additions & 0 deletions cmd/gotestwaf/helpers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package main

import (
"errors"
"fmt"
"net/url"
"regexp"
)

const (
httpProto = "http"
graphqlProto = httpProto
)

var (
ErrInvalidScheme = errors.New("invalid URL scheme")
ErrEmptyHost = errors.New("empty host")
)

// validateURL validates the given URL and URL scheme.
func validateURL(rawURL string, protocol string) (*url.URL, error) {
validURL, err := url.Parse(rawURL)
if err != nil {
return nil, err
}

re := regexp.MustCompile(fmt.Sprintf("^%ss?$", protocol))

if !re.MatchString(validURL.Scheme) {
return nil, ErrInvalidScheme
}

if validURL.Host == "" {
return nil, ErrEmptyHost
}

return validURL, nil
}

// checkOrCraftProtocolURL creates a URL from validHttpURL if the rawURL is empty
// or validates the rawURL.
func checkOrCraftProtocolURL(rawURL string, validHttpURL string, protocol string) (*url.URL, error) {
if rawURL != "" {
validURL, err := validateURL(rawURL, protocol)
if err != nil {
return nil, err
}

return validURL, nil
}

validURL, err := validateURL(validHttpURL, httpProto)
if err != nil {
return nil, err
}

scheme := protocol

if validURL.Scheme == "https" {
scheme += "s"
}

validURL.Scheme = scheme
validURL.Path = ""

return validURL, nil
}
1 change: 1 addition & 0 deletions cmd/gotestwaf/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ func run(ctx context.Context, cfg *config.Config, logger *logrus.Logger) error {
}

s.CheckGRPCAvailability(ctx)
s.CheckGraphQLAvailability(ctx)

err = s.Run(ctx)
if err != nil {
Expand Down
1 change: 1 addition & 0 deletions internal/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ type Config struct {
// Target settings
URL string `mapstructure:"url"`
GRPCPort uint16 `mapstructure:"grpcPort"`
GraphQLURL string `mapstructure:"graphqlURL"`
OpenAPIFile string `mapstructure:"openapiFile"`

// Test cases settings
Expand Down
3 changes: 2 additions & 1 deletion internal/db/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ type DB struct {
NumberOfTests uint
Hash string

IsGrpcAvailable bool
IsGrpcAvailable bool
IsGraphQLAvailable bool
}

func NewDB(tests []*Case) (*DB, error) {
Expand Down
4 changes: 3 additions & 1 deletion internal/db/statistics.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import (
)

type Statistics struct {
IsGrpcAvailable bool
IsGrpcAvailable bool
IsGraphQLAvailable bool

Paths ScannedPaths

Expand Down Expand Up @@ -137,6 +138,7 @@ func (db *DB) GetStatistics(ignoreUnresolved, nonBlockedAsPassed bool) *Statisti

s := &Statistics{
IsGrpcAvailable: db.IsGrpcAvailable,
IsGraphQLAvailable: db.IsGraphQLAvailable,
TestCasesFingerprint: db.Hash,
}

Expand Down
117 changes: 117 additions & 0 deletions internal/payload/placeholder/graphql.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package placeholder

import (
"crypto/sha256"
"net/http"
"net/url"
"strings"

"github.com/wallarm/gotestwaf/internal/scanner/types"

"github.com/pkg/errors"
)

type GraphQL struct {
name string
}

type GraphQLConfig struct {
Method string
}

var DefaultGraphQL = &GraphQL{name: "GraphQL"}

var _ Placeholder = (*GraphQL)(nil)

func (p *GraphQL) NewPlaceholderConfig(conf map[any]any) (PlaceholderConfig, error) {
result := &GraphQLConfig{}

method, ok := conf["method"]
if !ok {
return nil, &BadPlaceholderConfigError{
name: p.name,
err: errors.New("empty method"),
}
}
result.Method, ok = method.(string)
if !ok {
return nil, &BadPlaceholderConfigError{
name: p.name,
err: errors.Errorf("unknown type of 'method' field, expected string, got %T", method),
}
}

switch result.Method {
case http.MethodGet, http.MethodPost:
return result, nil

default:
return nil, &BadPlaceholderConfigError{
name: p.name,
err: errors.Errorf("unknown HTTP method, expected GET or POST, got %T", result.Method),
}
}
}

func (p *GraphQL) GetName() string {
return p.name
}

func (p *GraphQL) CreateRequest(requestURL, payload string, config PlaceholderConfig, httpClientType types.HTTPClientType) (types.Request, error) {
if httpClientType != types.GoHTTPClient {
return nil, errors.New("CreateRequest only support GoHTTPClient")
}

conf, ok := config.(*GraphQLConfig)
if !ok {
return nil, &BadPlaceholderConfigError{
name: p.name,
err: errors.Errorf("bad config type: got %T, expected: %T", config, &GraphQLConfig{}),
}
}

reqURL, err := url.Parse(requestURL)
if err != nil {
return nil, err
}

reqest := &types.GoHTTPRequest{}

switch conf.Method {
case http.MethodGet:
queryParams := reqURL.Query()
queryParams.Set("query", payload)
reqURL.RawQuery = queryParams.Encode()

req, err := http.NewRequest(http.MethodGet, reqURL.String(), nil)
if err != nil {
return nil, err
}

reqest.Req = req

return reqest, nil

case http.MethodPost:
req, err := http.NewRequest(http.MethodPost, reqURL.String(), strings.NewReader(payload))
if err != nil {
return nil, err
}

reqest.Req = req

return reqest, nil

default:
return nil, &BadPlaceholderConfigError{
name: p.name,
err: errors.Errorf("unknown HTTP method, expected GET or POST, got %T", conf.Method),
}
}
}

func (g *GraphQLConfig) Hash() []byte {
sha256sum := sha256.New()
sha256sum.Write([]byte(g.Method))
return sha256sum.Sum(nil)
}
8 changes: 4 additions & 4 deletions internal/payload/placeholder/grpc.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,14 @@ type GRPC struct {
name string
}

func (enc *GRPC) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
func (p *GRPC) NewPlaceholderConfig(map[any]any) (PlaceholderConfig, error) {
return nil, nil
}

func (enc *GRPC) GetName() string {
return enc.name
func (p *GRPC) GetName() string {
return p.name
}

func (enc *GRPC) CreateRequest(string, string, PlaceholderConfig, types.HTTPClientType) (types.Request, error) {
func (p *GRPC) CreateRequest(string, string, PlaceholderConfig, types.HTTPClientType) (types.Request, error) {
return nil, errors.New("not implemented")
}
1 change: 1 addition & 0 deletions internal/payload/placeholder/placeholder.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ type PlaceholderConfig interface {
var Placeholders map[string]Placeholder

var placeholders = []Placeholder{
DefaultGraphQL,
DefaultGRPC,
DefaultHeader,
DefaultHTMLForm,
Expand Down
35 changes: 27 additions & 8 deletions internal/report/chart.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,24 +127,43 @@ func generateChartData(s *db.Statistics) (

_, containsApiCat := counters["api"]

// Add gRPC counter if gRPC is unavailable to display it on graphic
if !s.IsGrpcAvailable && containsApiCat {
// gRPC is part of the API Security tests
counters["api"]["grpc"] = pair{}
if containsApiCat {
// Add gRPC counter if gRPC is unavailable to display it on graphic
if !s.IsGrpcAvailable {
// gRPC is part of the API Security tests
counters["api"]["grpc"] = pair{}
}

// Add GraphQL counter if GraphQL is unavailable to display it on graphic
if !s.IsGraphQLAvailable {
// GraphQL is part of the API Security tests
counters["api"]["graphql"] = pair{}
}
}

apiIndicators, apiItems = getIndicatorsAndItems(counters, "api")
appIndicators, appItems = getIndicatorsAndItems(counters, "app")

// Fix label for gRPC if it is unavailable
if !s.IsGrpcAvailable && containsApiCat {
fixIndicators := func(protocolName string) {
for i := 0; i < len(apiIndicators); i++ {
if strings.HasPrefix(apiIndicators[i], "grpc") {
apiIndicators[i] = "grpc (unavailable)"
if strings.HasPrefix(apiIndicators[i], protocolName) {
apiIndicators[i] = protocolName + " (unavailable)"
apiItems[i] = float64(0)
}
}
}

if containsApiCat {
// Fix label for gRPC if it is unavailable
if !s.IsGrpcAvailable {
fixIndicators("grpc")
}

// Fix label for GraphQL if it is unavailable
if !s.IsGraphQLAvailable {
fixIndicators("graphql")
}
}

return
}
22 changes: 11 additions & 11 deletions internal/report/html.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,27 +29,27 @@ var (
comparisonTable = []*report.ComparisonTableRow{
{
Name: "ModSecurity PARANOIA=1",
ApiSec: computeGrade(27.27, 1),
AppSec: computeGrade(66.79, 1),
OverallScore: computeGrade(47.03, 1),
ApiSec: computeGrade(42.86, 1),
AppSec: computeGrade(67.57, 1),
OverallScore: computeGrade(55.22, 1),
},
{
Name: "ModSecurity PARANOIA=2",
ApiSec: computeGrade(40.91, 1),
AppSec: computeGrade(58.23, 1),
OverallScore: computeGrade(49.57, 1),
ApiSec: computeGrade(57.14, 1),
AppSec: computeGrade(58.94, 1),
OverallScore: computeGrade(58.04, 1),
},
{
Name: "ModSecurity PARANOIA=3",
ApiSec: computeGrade(86.36, 1),
AppSec: computeGrade(50.39, 1),
OverallScore: computeGrade(68.38, 1),
ApiSec: computeGrade(85.71, 1),
AppSec: computeGrade(50.86, 1),
OverallScore: computeGrade(68.29, 1),
},
{
Name: "ModSecurity PARANOIA=4",
ApiSec: computeGrade(100.00, 1),
AppSec: computeGrade(36.03, 1),
OverallScore: computeGrade(68.02, 1),
AppSec: computeGrade(36.76, 1),
OverallScore: computeGrade(68.38, 1),
},
}

Expand Down
Loading