From c6c5abbe0f70351eaedfa5b10fe30b376c6c9da9 Mon Sep 17 00:00:00 2001 From: Emmanuel Gautier Date: Tue, 21 May 2024 16:52:15 +0200 Subject: [PATCH] feat: add more information in fingerprint discovery --- cmd/scan/discover.go | 1 + cmd/scan/report.go | 120 ++++++++++-- cmd/scan/root.go | 12 +- go.mod | 11 +- go.sum | 36 ++-- scan/discover/fingerprint/fingerprint.go | 133 +++++++++++++ scan/discover/fingerprint/fingerprint_test.go | 179 ++++++++++++++++++ .../server_signature/server_signature.go | 63 ------ .../server_signature/server_signature_test.go | 48 ----- scenario/common_scans.go | 4 +- scenario/discover.go | 4 +- 11 files changed, 445 insertions(+), 166 deletions(-) create mode 100644 scan/discover/fingerprint/fingerprint.go create mode 100644 scan/discover/fingerprint/fingerprint_test.go delete mode 100644 scan/discover/server_signature/server_signature.go delete mode 100644 scan/discover/server_signature/server_signature_test.go diff --git a/cmd/scan/discover.go b/cmd/scan/discover.go index ccdf4e0..2feb8bf 100644 --- a/cmd/scan/discover.go +++ b/cmd/scan/discover.go @@ -21,6 +21,7 @@ func NewDiscoverCmd() (scanCmd *cobra.Command) { ctx := cmd.Context() tracer := otel.Tracer("discover") baseUrl := args[0] + noFullReport = true analyticsx.TrackEvent(ctx, tracer, "Discover", []attribute.KeyValue{}) client := NewHTTPClientFromArgs(rateLimit, proxy, headers, cookies) diff --git a/cmd/scan/report.go b/cmd/scan/report.go index 586af39..a640548 100644 --- a/cmd/scan/report.go +++ b/cmd/scan/report.go @@ -8,6 +8,7 @@ import ( "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/fatih/color" "github.com/olekukonko/tablewriter" ) @@ -75,29 +76,109 @@ func severityTableColor(v *report.VulnerabilityReport) int { return tablewriter.BgWhiteColor } -func ContextualScanReport(reporter *report.Reporter) { +func WellKnownPathsScanReport(reporter *report.Reporter) { + openapiURL := "N/A" openapiReport := reporter.GetReportByID(discoverableopenapi.DiscoverableOpenAPIScanID) if openapiReport != nil && openapiReport.HasData() { openapiData, ok := openapiReport.Data.(discoverableopenapi.DiscoverableOpenAPIData) - if !ok { - fmt.Println("Failed to get OpenAPI data") - return + if ok { + openapiURL = openapiData.URL } - - fmt.Println("OpenAPI URL:", openapiData.URL) - } + graphqlURL := "N/A" graphqlReport := reporter.GetReportByID(discoverablegraphql.DiscoverableGraphQLPathScanID) if graphqlReport != nil && graphqlReport.HasData() { graphqlData, ok := graphqlReport.Data.(discoverablegraphql.DiscoverableGraphQLPathData) - if !ok { - fmt.Println("Failed to get GraphQL data") - return + if ok { + graphqlURL = graphqlData.URL } + } + + fmt.Println() + 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} + + table.Rich([]string{"OpenAPI", openapiURL}, tableColors) + table.Rich([]string{"GraphQL", graphqlURL}, tableColors) + + table.Render() +} + +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() + 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) + } - fmt.Println("GraphQL URL:", graphqlData.URL) + 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() } func DisplayReportTable(reporter *report.Reporter) { @@ -124,12 +205,7 @@ func DisplayReportTable(reporter *report.Reporter) { fmt.Println() headers := []string{"Operation", "Risk Level", "CVSS 4.0 Score", "OWASP", "Vulnerability"} - - table := tablewriter.NewWriter(outputStream) - table.SetHeader(headers) - table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) - table.SetCenterSeparator("|") - table.SetAutoMergeCellsByColumnIndex([]int{0}) + table := CreateTable(headers) vulnerabilityReports := NewFullScanVulnerabilityReports(reporter.GetReports()) for _, vulnReport := range vulnerabilityReports { @@ -165,6 +241,16 @@ func DisplayReportTable(reporter *report.Reporter) { table.Render() } +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/cmd/scan/root.go b/cmd/scan/root.go index 87f21b9..ea33e3e 100644 --- a/cmd/scan/root.go +++ b/cmd/scan/root.go @@ -1,8 +1,6 @@ package scan import ( - "fmt" - "github.com/cerberauth/vulnapi/report" "github.com/cerberauth/x/analyticsx" "github.com/spf13/cobra" @@ -12,6 +10,8 @@ import ( var reporter *report.Reporter +var noFullReport bool = false + func NewScanCmd() (scanCmd *cobra.Command) { scanCmd = &cobra.Command{ Use: "scan [type]", @@ -24,10 +24,12 @@ func NewScanCmd() (scanCmd *cobra.Command) { return } - fmt.Println() - fmt.Println() + WellKnownPathsScanReport(reporter) ContextualScanReport(reporter) - DisplayReportTable(reporter) + + if !noFullReport { + DisplayReportTable(reporter) + } analyticsx.TrackEvent(ctx, tracer, "Scan Report", []attribute.KeyValue{ attribute.Int("vulnerabilityCount", len(reporter.GetVulnerabilityReports())), diff --git a/go.mod b/go.mod index a3a1208..2e194f3 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/jarcoal/httpmock v1.3.1 github.com/olekukonko/tablewriter v0.0.5 + github.com/projectdiscovery/wappalyzergo v0.1.1 github.com/schollz/progressbar/v3 v3.14.2 github.com/spf13/cobra v1.8.0 github.com/std-uritemplate/std-uritemplate/go v0.0.56 @@ -68,11 +69,11 @@ require ( go.opentelemetry.io/otel/trace v1.26.0 // indirect go.opentelemetry.io/proto/otlp v1.2.0 // indirect golang.org/x/arch v0.7.0 // indirect - golang.org/x/crypto v0.22.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.19.0 // indirect - golang.org/x/term v0.19.0 // indirect - golang.org/x/text v0.14.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sys v0.20.0 // indirect + golang.org/x/term v0.20.0 // indirect + golang.org/x/text v0.15.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda // indirect google.golang.org/grpc v1.63.2 // indirect diff --git a/go.sum b/go.sum index ea9d0c0..b84f3d9 100644 --- a/go.sum +++ b/go.sum @@ -8,8 +8,6 @@ github.com/bytedance/sonic v1.11.3 h1:jRN+yEjakWh8aK5FzrciUHG8OFXK+4/KrAX/ysEtHA github.com/bytedance/sonic v1.11.3/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/cerberauth/x v0.0.0-20240417082024-19736ff88dc2 h1:3jY7GgzIZQeXgvGYdS60Tj4A/7w+mdadPbdgccAEhn4= -github.com/cerberauth/x v0.0.0-20240417082024-19736ff88dc2/go.mod h1:DjxEWTqK3M+i1luE3fNcxFMOadREoMXQKgRyfKsKkzE= github.com/cerberauth/x v0.0.0-20240518135044-19432dec4179 h1:bdjYP0CEsRZ1tmOKtPmY4yyx4pwWaI0q9v47i9WrozE= github.com/cerberauth/x v0.0.0-20240518135044-19432dec4179/go.mod h1:zYla5sUc0Z/FBEY555rxDCPX5vYPLZpGsqpEOd6u34w= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= @@ -63,8 +61,6 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0 h1:Wqo399gCIufwto+VfwCSvsnfGpF/w5E9CNxSwbpD6No= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.0/go.mod h1:qmOFXW2epJhM0qSnUUYpldc7gVz2KMQwJ/QYCDIa7XU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1 h1:/c3QmbOGMGTOumP2iT/rCwB7b0QDGLKzqOmktBjT+Is= github.com/grpc-ecosystem/grpc-gateway/v2 v2.19.1/go.mod h1:5SN9VR2LTsRFsrEC6FHgRbTWrTHu6tqPeKxEQv15giM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= @@ -116,6 +112,8 @@ github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX github.com/perimeterx/marshmallow v1.1.5/go.mod h1:dsXbUu8CRzfYP5a87xpp0xq9S3u0Vchtcl8we9tYaXw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/projectdiscovery/wappalyzergo v0.1.1 h1:HDxuqawatylDiOlfJf4IsabS0wA/Iyvqm7Dn18TVGjU= +github.com/projectdiscovery/wappalyzergo v0.1.1/go.mod h1:wBYGKmA5BQp/NWsAy1q/jSH8N1LHWQ/LV26DuR+KzPM= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= @@ -151,24 +149,16 @@ go.opentelemetry.io/contrib/propagators/b3 v1.26.0 h1:wgFbVA+bK2k+fGVfDOCOG4cfDA go.opentelemetry.io/contrib/propagators/b3 v1.26.0/go.mod h1:DDktFXxA+fyItAAM0Sbl5OBH7KOsCTjvbBdPKtoIf/k= go.opentelemetry.io/otel v1.26.0 h1:LQwgL5s/1W7YiiRwxf03QGnWLb2HW4pLiAhaA5cZXBs= go.opentelemetry.io/otel v1.26.0/go.mod h1:UmLkJHUAidDval2EICqBMbnAd0/m2vmpf/dAM+fvFs4= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0 h1:dT33yIHtmsqpixFsSQPwNeY5drM9wTcoL8h0FWF4oGM= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.25.0/go.mod h1:h95q0LBGh7hlAC08X2DhSeyIG02YQ0UyioTCVAqRPmc= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0 h1:1u/AyyOqAWzy+SkPxDpahCNZParHV8Vid1RnI2clyDE= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.26.0/go.mod h1:z46paqbJ9l7c9fIPCXTqTGwhQZ5XoTIsfeFYWboizjs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0 h1:Mbi5PKN7u322woPa85d7ebZ+SOvEoPvoiBu+ryHWgfA= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.25.0/go.mod h1:e7ciERRhZaOZXVjx5MiL8TK5+Xv7G5Gv5PA2ZDEJdL8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0 h1:1wp/gyxsuYtuE/JFxsQRtcCDtMrO2qMvlfXALU5wkzI= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.26.0/go.mod h1:gbTHmghkGgqxMomVQQMur1Nba4M0MQ8AYThXDUjsJ38= go.opentelemetry.io/otel/metric v1.26.0 h1:7S39CLuY5Jgg9CrnA9HHiEjGMF/X2VHvoXGgSllRz30= go.opentelemetry.io/otel/metric v1.26.0/go.mod h1:SY+rHOI4cEawI9a7N1A4nIg/nTQXe1ccCNWYOJUrpX4= -go.opentelemetry.io/otel/sdk v1.25.0 h1:PDryEJPC8YJZQSyLY5eqLeafHtG+X7FWnf3aXMtxbqo= -go.opentelemetry.io/otel/sdk v1.25.0/go.mod h1:oFgzCM2zdsxKzz6zwpTZYLLQsFwc+K0daArPdIhuxkw= go.opentelemetry.io/otel/sdk v1.26.0 h1:Y7bumHf5tAiDlRYFmGqetNcLaVUZmh4iYfmGxtmz7F8= go.opentelemetry.io/otel/sdk v1.26.0/go.mod h1:0p8MXpqLeJ0pzcszQQN4F0S5FVjBLgypeGSngLsmirs= go.opentelemetry.io/otel/trace v1.26.0 h1:1ieeAUb4y0TE26jUFrCIXKpTuVK7uJGN9/Z/2LP5sQA= go.opentelemetry.io/otel/trace v1.26.0/go.mod h1:4iDxvGDQuUkHve82hJJ8UqrwswHYsZuWCBllGV2U2y0= -go.opentelemetry.io/proto/otlp v1.1.0 h1:2Di21piLrCqJ3U3eXGCTPHE9R8Nh+0uglSnOyxikMeI= -go.opentelemetry.io/proto/otlp v1.1.0/go.mod h1:GpBHCBWiqvVLDqmHZsoMM3C5ySeKTC7ej/RNTae6MdY= go.opentelemetry.io/proto/otlp v1.2.0 h1:pVeZGk7nXDC9O2hncA6nHldxEjm6LByfA2aN8IOkz94= go.opentelemetry.io/proto/otlp v1.2.0/go.mod h1:gGpR8txAl5M03pDhMC79G6SdqNV26naRm/KDsgaHD8A= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= @@ -178,29 +168,27 @@ go.uber.org/ratelimit v0.3.1/go.mod h1:6euWsTB6U/Nb3X++xEUXA8ciPJvr19Q/0h1+oDcJh golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= +golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de h1:F6qOa9AZTYJXOUEr4jDysRDLrm4PHePlge4v4TGAlxY= google.golang.org/genproto v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:VUhTRKeHn9wwcdrk73nvdC9gF178Tzhmt/qyaFcPLSo= google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de h1:jFNzHPIeuzhdRwVhbZdiym9q0ory/xY3sA+v2wPg8I0= google.golang.org/genproto/googleapis/api v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:5iCWqnniDlqZHrd3neWVTOwvh/v6s3232omMecelax8= google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda h1:LI5DOvAxUPMv/50agcLLoo+AdWc1irS9Rzz4vPuD1V4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240401170217-c3f982113cda/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY= -google.golang.org/grpc v1.63.0 h1:WjKe+dnvABXyPJMD7KDNLxtoGk5tgk+YFWN6cBWjZE8= -google.golang.org/grpc v1.63.0/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= diff --git a/scan/discover/fingerprint/fingerprint.go b/scan/discover/fingerprint/fingerprint.go new file mode 100644 index 0000000..ecc7419 --- /dev/null +++ b/scan/discover/fingerprint/fingerprint.go @@ -0,0 +1,133 @@ +package fingerprint + +import ( + "io" + + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/internal/request" + "github.com/cerberauth/vulnapi/internal/scan" + "github.com/cerberauth/vulnapi/report" + wappalyzer "github.com/projectdiscovery/wappalyzergo" +) + +const ( + DiscoverFingerPrintScanID = "discover.server_signature" + DiscoverFingerPrintScanName = "Server Signature Discovery" +) + +type FingerPrintApp struct { + Name string `json:"name"` + Version *string `json:"version,omitempty"` +} + +type FingerPrintData struct { + CertificateAuthority []FingerPrintApp `json:"certificate_authority"` + Hosting []FingerPrintApp `json:"hosting"` + OS []FingerPrintApp `json:"os"` + Softwares []FingerPrintApp `json:"softwares"` + Databases []FingerPrintApp `json:"databases"` + Servers []FingerPrintApp `json:"servers"` + ServerExtensions []FingerPrintApp `json:"server_extensions"` + AuthServices []FingerPrintApp `json:"auth_services"` + CDNs []FingerPrintApp `json:"cdns"` + Caching []FingerPrintApp `json:"cache"` + Languages []FingerPrintApp `json:"languages"` + Frameworks []FingerPrintApp `json:"frameworks"` + SecurityServices []FingerPrintApp `json:"security_services"` +} + +var issue = report.Issue{ + ID: "discover.fingerprint", + Name: "Service Fingerprinting", + + Classifications: &report.Classifications{ + OWASP: report.OWASP_2023_SecurityMisconfiguration, + }, + + CVSS: report.CVSS{ + Version: 4.0, + Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N", + Score: 0, + }, +} + +func ScanHandler(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) { + vulnReport := report.NewVulnerabilityReport(issue).WithOperation(operation).WithSecurityScheme(securityScheme) + r := report.NewScanReport(DiscoverFingerPrintScanID, DiscoverFingerPrintScanName, operation) + + attempt, err := scan.ScanURL(operation, &securityScheme) + r.AddScanAttempt(attempt) + if err != nil { + return r.AddVulnerabilityReport(vulnReport.Skip()).End(), err + } + + if attempt.Err != nil { + return r.AddVulnerabilityReport(vulnReport.Skip()).End(), attempt.Err + } + + resp := attempt.Response + data, _ := io.ReadAll(resp.Body) + + wappalyzerClient, err := wappalyzer.New() + if err != nil { + return r.AddVulnerabilityReport(vulnReport.Skip()).End(), err + } + + fingerprints := wappalyzerClient.FingerprintWithInfo(resp.Header, data) + reportData := FingerPrintData{} + fingerPrintIdentifier := false + for name, fingerprint := range fingerprints { + if fingerprint.Categories == nil || len(fingerprint.Categories) == 0 { + continue + } + + for _, category := range fingerprint.Categories { + switch category { + case "SSL/TLS certificate authorities": + fingerPrintIdentifier = true + reportData.CertificateAuthority = append(reportData.CertificateAuthority, FingerPrintApp{Name: name}) + case "Operating systems": + fingerPrintIdentifier = true + reportData.OS = append(reportData.OS, FingerPrintApp{Name: name}) + case "Containers", "PaaS", "IaaS", "Hosting": + fingerPrintIdentifier = true + reportData.Hosting = append(reportData.Hosting, FingerPrintApp{Name: name}) + case "CMS", "Ecommerce", "Wikis", "Blogs", "LMS", "DMS", "Page builders", "Static site generator": + fingerPrintIdentifier = true + reportData.Softwares = append(reportData.Softwares, FingerPrintApp{Name: name}) + case "Databases": + fingerPrintIdentifier = true + reportData.Databases = append(reportData.Databases, FingerPrintApp{Name: name}) + case "Web servers", "Reverse proxies": + fingerPrintIdentifier = true + reportData.Servers = append(reportData.Servers, FingerPrintApp{Name: name}) + case "Web server extensions": + fingerPrintIdentifier = true + reportData.ServerExtensions = append(reportData.ServerExtensions, FingerPrintApp{Name: name}) + case "Authentication": + fingerPrintIdentifier = true + reportData.AuthServices = append(reportData.AuthServices, FingerPrintApp{Name: name}) + case "CDN": + fingerPrintIdentifier = true + reportData.CDNs = append(reportData.CDNs, FingerPrintApp{Name: name}) + case "Caching": + fingerPrintIdentifier = true + reportData.Caching = append(reportData.Caching, FingerPrintApp{Name: name}) + case "JavaScript frameworks", "Web frameworks": + fingerPrintIdentifier = true + reportData.Frameworks = append(reportData.Frameworks, FingerPrintApp{Name: name}) + case "Programming languages": + fingerPrintIdentifier = true + reportData.Languages = append(reportData.Languages, FingerPrintApp{Name: name}) + case "Security": + fingerPrintIdentifier = true + reportData.SecurityServices = append(reportData.SecurityServices, FingerPrintApp{Name: name}) + } + } + } + + vulnReport.WithBooleanStatus(!fingerPrintIdentifier) + r.WithData(reportData).AddVulnerabilityReport(vulnReport).End() + + return r, nil +} diff --git a/scan/discover/fingerprint/fingerprint_test.go b/scan/discover/fingerprint/fingerprint_test.go new file mode 100644 index 0000000..67baa2a --- /dev/null +++ b/scan/discover/fingerprint/fingerprint_test.go @@ -0,0 +1,179 @@ +package fingerprint_test + +import ( + "net/http" + "testing" + + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/internal/request" + fingerprint "github.com/cerberauth/vulnapi/scan/discover/fingerprint" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestCheckSignatureHeader_Failed_WithServerSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.Servers)) + assert.Equal(t, data.Servers[0].Name, "Apache HTTP Server:2.4.29") +} + +func TestCheckSignatureHeader_Failed_WithOSSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"Server": []string{"Ubuntu"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.OS)) + assert.Equal(t, data.OS[0].Name, "Ubuntu") +} + +func TestCheckSignatureHeader_Failed_WithHostingSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"platform": []string{"hostinger"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.Hosting)) + assert.Equal(t, data.Hosting[0].Name, "Hostinger") +} + +func TestCheckSignatureHeader_Failed_WithAuthenticationSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"x-auth0-requestid": []string{"id"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.AuthServices)) + assert.Equal(t, data.AuthServices[0].Name, "Auth0") +} + +func TestCheckSignatureHeader_Failed_WithCDNSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"cf-cache-status": []string{"HIT"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.CDNs)) + assert.Equal(t, data.CDNs[0].Name, "Cloudflare") +} + +func TestCheckSignatureHeader_Failed_WithLanguageSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"x-powered-by": []string{"PHP 7.4.3"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.Languages)) + assert.Equal(t, data.Languages[0].Name, "PHP") +} + +func TestCheckSignatureHeader_Failed_WithFrameworkSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil).HeaderAdd(http.Header{"x-powered-by": []string{"express"}})) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + data, _ := report.GetData().(fingerprint.FingerPrintData) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasFailed()) + assert.Equal(t, 1, len(data.Languages)) + assert.Equal(t, data.Languages[0].Name, "Node.js") + assert.Equal(t, 1, len(data.Frameworks)) + assert.Equal(t, data.Frameworks[0].Name, "Express") +} + +func TestCheckSignatureHeader_Passed_WithoutSignatureHeader(t *testing.T) { + client := request.DefaultClient + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + token := "token" + securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) + operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) + httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil)) + + report, err := fingerprint.ScanHandler(operation, securityScheme) + + require.NoError(t, err) + assert.Equal(t, 1, httpmock.GetTotalCallCount()) + assert.True(t, report.Vulns[0].HasPassed()) +} diff --git a/scan/discover/server_signature/server_signature.go b/scan/discover/server_signature/server_signature.go deleted file mode 100644 index 10fa42f..0000000 --- a/scan/discover/server_signature/server_signature.go +++ /dev/null @@ -1,63 +0,0 @@ -package serversignature - -import ( - "github.com/cerberauth/vulnapi/internal/auth" - "github.com/cerberauth/vulnapi/internal/request" - "github.com/cerberauth/vulnapi/internal/scan" - "github.com/cerberauth/vulnapi/report" -) - -const ( - DiscoverServerSignatureScanID = "discover.server_signature" - DiscoverServerSignatureScanName = "Server Signature Discovery" -) - -var issue = report.Issue{ - ID: "discover.server_signature", - Name: "Server Signature Exposed", - - Classifications: &report.Classifications{ - OWASP: report.OWASP_2023_SecurityMisconfiguration, - }, - - CVSS: report.CVSS{ - Version: 4.0, - Vector: "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:N/SI:N/SA:N", - Score: 0, - }, -} - -var signatureHeaders = []string{"Server", "X-Powered-By", "X-AspNet-Version", "X-AspNetMvc-Version"} - -func checkSignatureHeader(headers map[string][]string) bool { - for _, header := range signatureHeaders { - value := headers[header] - if len(value) > 0 { - return false - } - } - - return true -} - -func ScanHandler(operation *request.Operation, securityScheme auth.SecurityScheme) (*report.ScanReport, error) { - vulnReport := report.NewVulnerabilityReport(issue).WithOperation(operation).WithSecurityScheme(securityScheme) - r := report.NewScanReport(DiscoverServerSignatureScanID, DiscoverServerSignatureScanName, operation) - - vsa, err := scan.ScanURL(operation, &securityScheme) - r.AddScanAttempt(vsa) - if err != nil { - r.AddVulnerabilityReport(vulnReport.Skip()).End() - return r, err - } - - if vsa.Err != nil { - r.AddVulnerabilityReport(vulnReport.Skip()).End() - return r, vsa.Err - } - - vulnReport.WithBooleanStatus(checkSignatureHeader(vsa.Response.Header)) - r.AddVulnerabilityReport(vulnReport).End() - - return r, nil -} diff --git a/scan/discover/server_signature/server_signature_test.go b/scan/discover/server_signature/server_signature_test.go deleted file mode 100644 index 4854605..0000000 --- a/scan/discover/server_signature/server_signature_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package serversignature_test - -import ( - "net/http" - "testing" - - "github.com/cerberauth/vulnapi/internal/auth" - "github.com/cerberauth/vulnapi/internal/request" - serversignature "github.com/cerberauth/vulnapi/scan/discover/server_signature" - "github.com/jarcoal/httpmock" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestCheckSignatureHeader_Failed_WithSignatureHeader(t *testing.T) { - client := request.DefaultClient - httpmock.ActivateNonDefault(client.Client) - defer httpmock.DeactivateAndReset() - - token := "token" - securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) - operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) - - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil).HeaderAdd(http.Header{"Server": []string{"Apache/2.4.29 (Ubuntu)"}})) - - report, err := serversignature.ScanHandler(operation, securityScheme) - - require.NoError(t, err) - assert.Equal(t, 1, httpmock.GetTotalCallCount()) - assert.True(t, report.Vulns[0].HasFailed()) -} - -func TestCheckSignatureHeader_Passed_WithoutSignatureHeader(t *testing.T) { - client := request.DefaultClient - httpmock.ActivateNonDefault(client.Client) - defer httpmock.DeactivateAndReset() - - token := "token" - securityScheme := auth.NewAuthorizationBearerSecurityScheme("default", &token) - operation, _ := request.NewOperation(client, http.MethodGet, "http://localhost:8080/", nil, nil, nil) - httpmock.RegisterResponder(operation.Method, operation.Request.URL.String(), httpmock.NewBytesResponder(204, nil)) - - report, err := serversignature.ScanHandler(operation, securityScheme) - - require.NoError(t, err) - assert.Equal(t, 1, httpmock.GetTotalCallCount()) - assert.True(t, report.Vulns[0].HasPassed()) -} diff --git a/scenario/common_scans.go b/scenario/common_scans.go index 99df20e..99c96a7 100644 --- a/scenario/common_scans.go +++ b/scenario/common_scans.go @@ -9,7 +9,7 @@ import ( nullsignature "github.com/cerberauth/vulnapi/scan/broken_authentication/jwt/null_signature" weaksecret "github.com/cerberauth/vulnapi/scan/broken_authentication/jwt/weak_secret" acceptunauthenticated "github.com/cerberauth/vulnapi/scan/discover/accept_unauthenticated" - serversignature "github.com/cerberauth/vulnapi/scan/discover/server_signature" + fingerprint "github.com/cerberauth/vulnapi/scan/discover/fingerprint" httpcookies "github.com/cerberauth/vulnapi/scan/misconfiguration/http_cookies" httpheaders "github.com/cerberauth/vulnapi/scan/misconfiguration/http_headers" httptrace "github.com/cerberauth/vulnapi/scan/misconfiguration/http_trace" @@ -17,7 +17,7 @@ import ( ) func WithAllCommonScans(s *scan.Scan) *scan.Scan { - s.AddScanHandler(serversignature.ScanHandler) + s.AddScanHandler(fingerprint.ScanHandler) s.AddOperationScanHandler(acceptunauthenticated.ScanHandler) s.AddOperationScanHandler(authenticationbypass.ScanHandler) diff --git a/scenario/discover.go b/scenario/discover.go index efa249a..2eb5d8b 100644 --- a/scenario/discover.go +++ b/scenario/discover.go @@ -7,7 +7,7 @@ import ( "github.com/cerberauth/vulnapi/scan" discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" - serversignature "github.com/cerberauth/vulnapi/scan/discover/server_signature" + fingerprint "github.com/cerberauth/vulnapi/scan/discover/fingerprint" ) func NewDiscoverScan(method string, url string, client *request.Client, reporter *report.Reporter) (*scan.Scan, error) { @@ -26,7 +26,7 @@ func NewDiscoverScan(method string, url string, client *request.Client, reporter return nil, err } - urlScan.AddScanHandler(discoverableopenapi.ScanHandler).AddScanHandler(discoverablegraphql.ScanHandler).AddScanHandler(serversignature.ScanHandler) + urlScan.AddScanHandler(fingerprint.ScanHandler).AddScanHandler(discoverableopenapi.ScanHandler).AddScanHandler(discoverablegraphql.ScanHandler) return urlScan, nil }