diff --git a/CHANGELOG.md b/CHANGELOG.md index f46218e..d148d89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,7 @@ # Changelog for SSLChecker # v1.0.0 -- Initial Release \ No newline at end of file +- Initial Release + +# v1.1.0 +- Added the ability to pass a port to SSLScanner \ No newline at end of file diff --git a/README.md b/README.md index 0c4aad1..be781fd 100644 --- a/README.md +++ b/README.md @@ -14,16 +14,18 @@ Deployment - As part of the above setup, you will be able to deploy to Azure usi Invoke the function on the command line using curl: -``` curl http://.azurewebsite.net/api/{scan:alpha}/{view:alpha}/{name}``` +``` curl http://.azurewebsite.net/api/{scan:alpha}/{view:alpha}/{name}/{port}``` -There are three parts to pass to the URI: scan, view, and name. +There are four parts to pass to the URI: scan, view, name and port. "scan" is the type of scan: policy or full. Currently, the default policy prohibits using SSL 2.0/3.0 and TLS 1.0, so the policy scan will identify which unsupported ciphers are in use, if any. A full scan will report back all supported ciphers. In a future release I will make this configurable. -Since corporations often use [split-view DNS](https://en.wikipedia.org/wiki/Split-horizon_DNS), "view" in this context is the network viewpoint you want to scan, either internal or external. This is accomplished by specifying a valid DNS server to use for name resolution. The default value for external will use OpenDNS (e.g. 208.67.222.222). The default for internal will be 0.0.0.0 and will result in an error if a scan is attempted and no internal DNS server is specified. +Since corporations often use [split-view DNS](https://en.wikipedia.org/wiki/Split-horizon_DNS), "view" in this context is the network viewpoint you want to scan, either internal or external. This is accomplished by specifying a valid DNS server to use for name resolution. The default value for external will use OpenDNS (e.g. 208.67.222.222). The default for internal will be 0.0.0.0 and will result in an error if a scan is attempted and no internal DNS server is specified. Please modify the config.ini file to use an internal DNS server. "name" should be the DNS domain name you would like to scan (i.e., github.com). +"port" is optional and if omitted will default to TCP 443. + ## A Note on Authentication Microsoft has extensive documentation on how to secure an HTTP endpoint in Azure Functions [here](https://docs.microsoft.com/en-us/azure/azure-functions/functions-bindings-http-webhook-trigger?tabs=csharp#secure-an-http-endpoint-in-production). There are two main ways to secure a function: Turn on App Service Authentication/Authorization for the function app, or use Azure API Management (APIM) to authenticate requests. Additionally, Azure functions support API key authorization that you can supply either as a query string variable or in a HTTP header. Microsoft states that API key authorization is not intended as a way to secure an HTTP trigger in production diff --git a/SSLChecker/SSLChecker/__init__.py b/SSLChecker/SSLChecker/__init__.py index 686a964..a656645 100644 --- a/SSLChecker/SSLChecker/__init__.py +++ b/SSLChecker/SSLChecker/__init__.py @@ -24,6 +24,18 @@ def main(req: func.HttpRequest) -> func.HttpResponse: scan_type = req.route_params.get('scan') view = req.route_params.get('view') name = req.route_params.get('name') + port = req.route_params.get('port') + + # Port is optional and will default to 443 if none is provided + if port is None: + port = 443 + elif port.isnumeric() is False: + error = {"Message": ("Please pass a valid port")} + return json.dumps(error) + elif int(port) > 65535: + error = {"Message": ("Please pass a valid port" + " in range 0-65535")} + return json.dumps(error) """ Check to ensure ALL parameters were passed in the URI. If you mark the route parameters in function.json as mandatory, @@ -82,7 +94,7 @@ def main(req: func.HttpRequest) -> func.HttpResponse: return json.dumps(error) # Run the scan - scanjob = scanner.scan(name, ip, view, scan_type) + scanjob = scanner.scan(name, ip, port, view, scan_type) elapsedtime = process_time() - starttime logging.info(f'{name} processed for {elapsedtime}') return json.dumps(scanjob) diff --git a/SSLChecker/SSLChecker/function.json b/SSLChecker/SSLChecker/function.json index 8e46469..ebc21f1 100644 --- a/SSLChecker/SSLChecker/function.json +++ b/SSLChecker/SSLChecker/function.json @@ -6,7 +6,7 @@ "type": "httpTrigger", "direction": "in", "name": "req", - "route": "{scan:alpha?}/{view:alpha?}/{name?}", + "route": "{scan:alpha?}/{view:alpha?}/{name?}/{port?}", "methods": [ "get" ] diff --git a/SSLChecker/sharedcode/scanner.py b/SSLChecker/sharedcode/scanner.py index a4d3896..d3d2bd7 100644 --- a/SSLChecker/sharedcode/scanner.py +++ b/SSLChecker/sharedcode/scanner.py @@ -24,15 +24,15 @@ SynchronousScanner.DEFAULT_NETWORK_TIMEOUT = 3 -def scan(name, ip, view, suite): - """ Two inputs: web site name and ip - TODO: extend this to include port """ +def scan(name, ip, port, view, suite): + """ Five inputs: web site name, ip, port + split-dns view, and cipher suite """ try: server_tester = ServerConnectivityTester( hostname=name, ip_address=ip, - port=443, + port=port, tls_wrapped_protocol=TlsWrappedProtocolEnum.HTTPS ) # This line checks to see if the host is online @@ -41,7 +41,7 @@ def scan(name, ip, view, suite): # Could not establish an SSL connection to the server except ConnectionToServerTimedOut: error = results.set_error(f'{name}', - "Connection to TCP 443 timed-out") + f"Connection to TCP {port} timed-out") return error except ServerConnectivityError: error = results.set_error(f'{name}', diff --git a/tests/test_SSLChecker.py b/tests/test_SSLChecker.py index 1d05ea2..e66c3d1 100644 --- a/tests/test_SSLChecker.py +++ b/tests/test_SSLChecker.py @@ -264,3 +264,69 @@ def test_missing_policy_view_dns_name(): " valid scan type: policy or full, " "valid DNS view: internal or external, " "and a valid DNS domain name") + + +def test_external_bad_port(): + # Construct a mock HTTP request + req = func.HttpRequest( + method='GET', + body=None, + url='/api/', + route_params={'scan': 'policy', + 'view': 'external', + 'name': 'yahoo.com', + 'port': 'a'} + ) + + # Call the function + resp = main(req) + + # Convert resp string to dict + results = json.loads(resp) + + # Check the output to ensure the DNS name could not resolve + assert results["Message"] == 'Please pass a valid port' + + +def test_external_port_timeout(): + # Construct a mock HTTP request + req = func.HttpRequest( + method='GET', + body=None, + url='/api/', + route_params={'scan': 'policy', + 'view': 'external', + 'name': 'yahoo.com', + 'port': '8443'} + ) + + # Call the function + resp = main(req) + + # Convert resp string to dict + results = json.loads(resp) + + # Check the output to ensure the DNS name could not resolve + assert results["Message"] == 'Connection to TCP 8443 timed-out' + + +def test_external_port_not_in_range(): + # Construct a mock HTTP request + req = func.HttpRequest( + method='GET', + body=None, + url='/api/', + route_params={'scan': 'policy', + 'view': 'external', + 'name': 'espn.com', + 'port': '123456'} + ) + + # Call the function + resp = main(req) + + # Convert resp string to dict + results = json.loads(resp) + + # Check the output to ensure the DNS name could not resolve + assert results["Message"] == 'Please pass a valid port in range 0-65535'