From a6c61b8ecb2e45584fecc16843e8e0defc85ca3b Mon Sep 17 00:00:00 2001 From: Emmanuel Gautier Date: Wed, 1 Jan 2025 22:00:16 +0100 Subject: [PATCH] feat: add healthcheck endpoints discovery scan --- .../cmd/printtable/wellknown_paths_table.go | 4 ++ scan/discover/healthcheck/healthcheck.go | 36 +++++++++++++ scan/discover/healthcheck/healthcheck_test.go | 50 +++++++++++++++++++ scan/discover/well-known/well_known.go | 2 - scenario/discover_api.go | 4 +- seclist/lists/healthcheck.txt | 12 +++++ seclist/lists/well-known.txt | 2 +- 7 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 scan/discover/healthcheck/healthcheck.go create mode 100644 scan/discover/healthcheck/healthcheck_test.go create mode 100644 seclist/lists/healthcheck.txt diff --git a/internal/cmd/printtable/wellknown_paths_table.go b/internal/cmd/printtable/wellknown_paths_table.go index 6eb6c79..7ea2400 100644 --- a/internal/cmd/printtable/wellknown_paths_table.go +++ b/internal/cmd/printtable/wellknown_paths_table.go @@ -8,6 +8,7 @@ import ( discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" exposedfiles "github.com/cerberauth/vulnapi/scan/discover/exposed_files" + "github.com/cerberauth/vulnapi/scan/discover/healthcheck" wellknown "github.com/cerberauth/vulnapi/scan/discover/well-known" "github.com/olekukonko/tablewriter" ) @@ -41,6 +42,9 @@ func WellKnownPathsScanReport(reporter *report.Reporter) { exposedFiles := reporter.GetScanReportByID(exposedfiles.DiscoverableFilesScanID) rows = append(rows, wellKnownPathsFromReport(exposedFiles, "Exposed Files")...) + healthcheckEndpoints := reporter.GetScanReportByID(healthcheck.DiscoverableHealthCheckScanID) + rows = append(rows, wellKnownPathsFromReport(healthcheckEndpoints, "Health Check")...) + if len(rows) == 0 { return } diff --git a/scan/discover/healthcheck/healthcheck.go b/scan/discover/healthcheck/healthcheck.go new file mode 100644 index 0000000..40f88dd --- /dev/null +++ b/scan/discover/healthcheck/healthcheck.go @@ -0,0 +1,36 @@ +package healthcheck + +import ( + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/internal/operation" + "github.com/cerberauth/vulnapi/report" + "github.com/cerberauth/vulnapi/scan/discover" +) + +const ( + DiscoverableHealthCheckScanID = "discover.healthcheck" + DiscoverableHealthCheckScanName = "Discoverable healthcheck endpoint" +) + +var issue = report.Issue{ + ID: "discover.discoverable_healthcheck", + Name: "Discoverable healthcheck endpoint", + + Classifications: &report.Classifications{ + OWASP: report.OWASP_2023_SSRF, + }, + + 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 healthcheckSeclistUrl = "https://raw.githubusercontent.com/cerberauth/vulnapi/main/seclist/lists/healthcheck.txt" + +func ScanHandler(op *operation.Operation, securityScheme *auth.SecurityScheme) (*report.ScanReport, error) { + vulnReport := report.NewIssueReport(issue).WithOperation(op).WithSecurityScheme(securityScheme) + r := report.NewScanReport(DiscoverableHealthCheckScanID, DiscoverableHealthCheckScanName, op) + return discover.DownloadAndScanURLs("HealthCheck", healthcheckSeclistUrl, r, vulnReport, op, securityScheme) +} diff --git a/scan/discover/healthcheck/healthcheck_test.go b/scan/discover/healthcheck/healthcheck_test.go new file mode 100644 index 0000000..e9c541f --- /dev/null +++ b/scan/discover/healthcheck/healthcheck_test.go @@ -0,0 +1,50 @@ +package healthcheck_test + +import ( + "net/http" + "testing" + + "github.com/cerberauth/vulnapi/internal/auth" + "github.com/cerberauth/vulnapi/internal/operation" + "github.com/cerberauth/vulnapi/internal/request" + "github.com/cerberauth/vulnapi/scan/discover/healthcheck" + "github.com/jarcoal/httpmock" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestDiscoverableScanner_Passed_WhenNoDiscoverableHealthCheckEndpointFound(t *testing.T) { + client := request.NewClient(request.NewClientOptions{ + RateLimit: 500, + }) + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + op := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/", nil, client) + httpmock.RegisterResponder(op.Method, op.URL.String(), httpmock.NewBytesResponder(http.StatusNoContent, nil)) + httpmock.RegisterNoResponder(httpmock.NewBytesResponder(http.StatusNotFound, nil)) + + report, err := healthcheck.ScanHandler(op, auth.MustNewNoAuthSecurityScheme()) + + require.NoError(t, err) + assert.Greater(t, httpmock.GetTotalCallCount(), 5) + assert.True(t, report.Issues[0].HasPassed()) +} + +func TestDiscoverableScanner_Failed_WhenOneHealthCheckEndpointFound(t *testing.T) { + client := request.NewClient(request.NewClientOptions{ + RateLimit: 500, + }) + httpmock.ActivateNonDefault(client.Client) + defer httpmock.DeactivateAndReset() + + operation := operation.MustNewOperation(http.MethodGet, "http://localhost:8080/healthz", nil, client) + httpmock.RegisterResponder(operation.Method, operation.URL.String(), httpmock.NewBytesResponder(http.StatusOK, nil)) + httpmock.RegisterNoResponder(httpmock.NewBytesResponder(http.StatusNotFound, nil)) + + report, err := healthcheck.ScanHandler(operation, auth.MustNewNoAuthSecurityScheme()) + + require.NoError(t, err) + assert.Greater(t, httpmock.GetTotalCallCount(), 0) + assert.True(t, report.Issues[0].HasFailed()) +} diff --git a/scan/discover/well-known/well_known.go b/scan/discover/well-known/well_known.go index 5968e46..f40481c 100644 --- a/scan/discover/well-known/well_known.go +++ b/scan/discover/well-known/well_known.go @@ -12,8 +12,6 @@ const ( DiscoverableWellKnownScanName = "Discoverable well-known path" ) -type DiscoverableGraphQLPathData = discover.DiscoverData - var issue = report.Issue{ ID: "discover.discoverable_well_known", Name: "Discoverable well-known path", diff --git a/scenario/discover_api.go b/scenario/discover_api.go index cf9208d..7cb05c9 100644 --- a/scenario/discover_api.go +++ b/scenario/discover_api.go @@ -7,7 +7,8 @@ import ( discoverablegraphql "github.com/cerberauth/vulnapi/scan/discover/discoverable_graphql" discoverableopenapi "github.com/cerberauth/vulnapi/scan/discover/discoverable_openapi" exposedfiles "github.com/cerberauth/vulnapi/scan/discover/exposed_files" - fingerprint "github.com/cerberauth/vulnapi/scan/discover/fingerprint" + "github.com/cerberauth/vulnapi/scan/discover/fingerprint" + "github.com/cerberauth/vulnapi/scan/discover/healthcheck" wellknown "github.com/cerberauth/vulnapi/scan/discover/well-known" ) @@ -37,6 +38,7 @@ func NewDiscoverAPIScan(method string, url string, client *request.Client, opts urlScan.AddScanHandler(scan.NewOperationScanHandler(discoverablegraphql.DiscoverableGraphQLPathScanID, discoverablegraphql.ScanHandler)) urlScan.AddScanHandler(scan.NewOperationScanHandler(exposedfiles.DiscoverableFilesScanID, exposedfiles.ScanHandler)) urlScan.AddScanHandler(scan.NewOperationScanHandler(wellknown.DiscoverableWellKnownScanID, wellknown.ScanHandler)) + urlScan.AddScanHandler(scan.NewOperationScanHandler(healthcheck.DiscoverableHealthCheckScanID, healthcheck.ScanHandler)) return urlScan, nil } diff --git a/seclist/lists/healthcheck.txt b/seclist/lists/healthcheck.txt new file mode 100644 index 0000000..0159923 --- /dev/null +++ b/seclist/lists/healthcheck.txt @@ -0,0 +1,12 @@ +alive +health +health/live +health/ready +health/started +healthz +healthz/live +healthz/ready +ready +status +status/live +status/ready \ No newline at end of file diff --git a/seclist/lists/well-known.txt b/seclist/lists/well-known.txt index 6c6a147..9f07414 100644 --- a/seclist/lists/well-known.txt +++ b/seclist/lists/well-known.txt @@ -3,4 +3,4 @@ .well-known/jwks.json .well-known/oauth-authorization-server .well-known/openid-configuration -.well-known/openid-federation +.well-known/openid-federation \ No newline at end of file