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

AppProtect gRPC automation tests #1603

Merged
merged 10 commits into from
Jun 3, 2021
Merged
Show file tree
Hide file tree
Changes from 3 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
Binary file added tests/binaries/grpc_client
Binary file not shown.
32 changes: 32 additions & 0 deletions tests/data/appprotect/grpc/grpc-block-saygoodbye.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: appprotect.f5.com/v1beta1
kind: APPolicy
metadata:
name: grpc-block-saygoodbye
spec:
policy:
blocking-settings:
violations:
- name: VIOL_GRPC_METHOD
block: true
alarm: true
applicationLanguage: utf-8
bot-defense:
settings:
isEnabled: false
grpc-profiles:
- associateUrls: true
defenseAttributes:
allowUnknownFields: false
maximumDataLength: "10000"
description: My first profile
idlFiles:
- idlFile:
fileName: autheid.proto
name: gProf1
idl-files:
- isBase64: true
fileName: autheid.proto
contents: Ly8gVGhlIGdyZWV0aW5nIHNlcnZpY2UgZGVmaW5pdGlvbi4KCnN5bnRheCA9ICJwcm90bzMiOwoKcGFja2FnZSBoZWxsb3dvcmxkOwoKc2VydmljZSBHcmVldGVyIHsKICAvLyBTZW5kcyBhIGdyZWV0aW5nCiAgcnBjIFNheUhlbGxvIChIZWxsb1JlcXVlc3QpIHJldHVybnMgKEhlbGxvUmVwbHkpIHt9Cn0KCi8vIFRoZSByZXF1ZXN0IG1lc3NhZ2UgY29udGFpbmluZyB0aGUgdXNlcidzIG5hbWUuCm1lc3NhZ2UgSGVsbG9SZXF1ZXN0IHsKICBzdHJpbmcgbmFtZSA9IDE7Cn0KCi8vIFRoZSByZXNwb25zZSBtZXNzYWdlIGNvbnRhaW5pbmcgdGhlIGdyZWV0aW5ncwptZXNzYWdlIEhlbGxvUmVwbHkgewogIHN0cmluZyBtZXNzYWdlID0gMTsKfQo=
name: valid_string_encoding_policy
template:
name: POLICY_TEMPLATE_NGINX_BASE
32 changes: 32 additions & 0 deletions tests/data/appprotect/grpc/grpc-block-sayhello.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: appprotect.f5.com/v1beta1
kind: APPolicy
metadata:
name: grpc-block-sayhello
spec:
policy:
blocking-settings:
violations:
- name: VIOL_GRPC_METHOD
block: True
alarm: True
applicationLanguage: utf-8
bot-defense:
settings:
isEnabled: false
grpc-profiles:
- associateUrls: true
defenseAttributes:
allowUnknownFields: false
maximumDataLength: "10000"
description: My first profile
idlFiles:
- idlFile:
fileName: autheid.proto
name: gProf1
idl-files:
- isBase64: true
fileName: autheid.proto
contents: Ly8gVGhlIGdyZWV0aW5nIHNlcnZpY2UgZGVmaW5pdGlvbi4KCnN5bnRheCA9ICJwcm90bzMiOwoKcGFja2FnZSBoZWxsb3dvcmxkOwoKc2VydmljZSBHcmVldGVyIHsKICAvLyBTZW5kcyBhIGdyZWV0aW5nCiAgcnBjIFNheUdvb2RieWUgKEhlbGxvUmVxdWVzdCkgcmV0dXJucyAoSGVsbG9SZXBseSkge30KfQoKLy8gVGhlIHJlcXVlc3QgbWVzc2FnZSBjb250YWluaW5nIHRoZSB1c2VyJ3MgbmFtZS4KbWVzc2FnZSBIZWxsb1JlcXVlc3QgewogIHN0cmluZyBuYW1lID0gMTsKfQoKLy8gVGhlIHJlc3BvbnNlIG1lc3NhZ2UgY29udGFpbmluZyB0aGUgZ3JlZXRpbmdzCm1lc3NhZ2UgSGVsbG9SZXBseSB7CiAgc3RyaW5nIG1lc3NhZ2UgPSAxOwp9Cg==
name: valid_string_encoding_policy
template:
name: POLICY_TEMPLATE_NGINX_BASE
25 changes: 25 additions & 0 deletions tests/data/appprotect/grpc/ingress.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: appprotect-ingress
annotations:
nginx.org/grpc-services: "grpc-svc"
kubernetes.io/ingress.class: "nginx"
appprotect.f5.com/app-protect-policy: "default/grpc"
appprotect.f5.com/app-protect-enable: "True"
appprotect.f5.com/app-protect-security-log-enable: "True"
appprotect.f5.com/app-protect-security-log: "default/logconf"
appprotect.f5.com/app-protect-security-log-destination: "syslog:server=172.17.0.10:514"
spec:
tls:
- hosts:
- appprotect.example.com
secretName: appprotect-secret
rules:
- host: appprotect.example.com
http:
paths:
- path: /helloworld.Greeter
backend:
serviceName: grpc-svc
servicePort: 50051
7 changes: 7 additions & 0 deletions tests/data/appprotect/grpc/nginx-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
kind: ConfigMap
apiVersion: v1
metadata:
name: nginx-config
namespace: nginx-ingress
data:
http2: "True"
32 changes: 32 additions & 0 deletions tests/data/common/app/grpc/app.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: grpc
spec:
replicas: 1
selector:
matchLabels:
app: greeter
template:
metadata:
labels:
app: greeter
spec:
containers:
- name: greeter
image: nginxkic/test-grpc-server:0.1
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should we publish the sources similarly to how we published the sources for the TCP and UDP servers? so we don't loose it

Copy link
Contributor Author

@vepatel vepatel May 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not too sure about publishing sources for this one as the code isn't written by our team members unlike TCP/UDP server one which @soneillf5 wrote. Also If we're to add source in the tests, i'd prefer adding them to examples/ first.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

in the case of the backend, it's just a Dockerfile, right? we don't really modify the backend code?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

where does the test-grpc-server come from then ?

ports:
- containerPort: 50051
---
apiVersion: v1
kind: Service
metadata:
name: grpc-svc
spec:
ports:
- port: 50051
targetPort: 50051
protocol: TCP
name: grpc
selector:
app: greeter
3 changes: 2 additions & 1 deletion tests/requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,5 @@ urllib3==1.26.4
pytest-html==3.1.1
pytest-profiling==1.7.0
more-itertools==8.7.0
mock==4.0.3
mock==4.0.3
python-hosts==1.0.1
171 changes: 171 additions & 0 deletions tests/suite/test_app_protect_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
import subprocess
import pytest
from python_hosts import Hosts, HostsEntry
from settings import TEST_DATA, DEPLOYMENTS
from suite.custom_resources_utils import (
create_ap_logconf_from_yaml,
create_ap_policy_from_yaml,
delete_ap_policy,
delete_ap_logconf,
)
from suite.resources_utils import (
wait_before_test,
create_example_app,
wait_until_all_pods_are_ready,
create_items_from_yaml,
delete_items_from_yaml,
delete_common_app,
replace_configmap_from_yaml,
create_ingress_with_ap_annotations,
wait_before_test,
get_file_contents,
)
from suite.yaml_utils import get_first_ingress_host_from_yaml

my_hosts = Hosts()
vepatel marked this conversation as resolved.
Show resolved Hide resolved
log_loc = f"/var/log/messages"
valid_resp_txt = "Hello"
invalid_resp_text = "The request was rejected. Please consult with your administrator."

class BackendSetup:
"""
Encapsulate the example details.

Attributes:
ingress_host (str):
"""

def __init__(self, ingress_host, ssl_port):
self.ingress_host = ingress_host
self.ssl_port = ssl_port


@pytest.fixture(scope="function")
def backend_setup(request, kube_apis, ingress_controller_endpoint, ingress_controller_prerequisites, test_namespace) -> BackendSetup:
"""
Deploy a simple application and AppProtect manifests.

:param request: pytest fixture
:param kube_apis: client apis
:param ingress_controller_endpoint: public endpoint
:param test_namespace:
:return: BackendSetup
"""
print("------------------------- Replace ConfigMap with HTTP2 -------------------------")
replace_configmap_from_yaml(kube_apis.v1,
ingress_controller_prerequisites.config_map['metadata']['name'],
ingress_controller_prerequisites.namespace,
f"{TEST_DATA}/appprotect/grpc/nginx-config.yaml")

policy = request.param["policy"]
print("------------------------- Deploy backend application -------------------------")
create_example_app(kube_apis, "grpc", test_namespace)
wait_until_all_pods_are_ready(kube_apis.v1, test_namespace)

print("------------------------- Deploy Secret -----------------------------")
src_sec_yaml = f"{TEST_DATA}/appprotect/appprotect-secret.yaml"
create_items_from_yaml(kube_apis, src_sec_yaml, test_namespace)

print("------------------------- Deploy logconf -----------------------------")
src_log_yaml = f"{TEST_DATA}/appprotect/logconf.yaml"
log_name = create_ap_logconf_from_yaml(kube_apis.custom_objects, src_log_yaml, test_namespace)

print(f"------------------------- Deploy appolicy: {policy} ---------------------------")
src_pol_yaml = f"{TEST_DATA}/appprotect/grpc/{policy}.yaml"
pol_name = create_ap_policy_from_yaml(kube_apis.custom_objects, src_pol_yaml, test_namespace)

print("------------------------- Deploy Syslog -----------------------------")
src_syslog_yaml = f"{TEST_DATA}/appprotect/syslog.yaml"
create_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace)
wait_until_all_pods_are_ready(kube_apis.v1, test_namespace)
wait_before_test(10)
syslog_ep = (
kube_apis.v1.read_namespaced_endpoints("syslog-svc", test_namespace)
.subsets[0]
.addresses[0]
.ip
)
print(syslog_ep)
print("------------------------- Deploy ingress -----------------------------")
ingress_host = {}
src_ing_yaml = f"{TEST_DATA}/appprotect/grpc/ingress.yaml"
create_ingress_with_ap_annotations(kube_apis, src_ing_yaml, test_namespace, policy, "True", "True", f"{syslog_ep}:514")
ingress_host = get_first_ingress_host_from_yaml(src_ing_yaml)
node_ip = request.config.getoption("--node-ip", None)
new_entry = HostsEntry(entry_type='ipv4', address=node_ip, names=[ingress_host])
print(new_entry)
my_hosts.add([new_entry])
wait_before_test(40)
vepatel marked this conversation as resolved.
Show resolved Hide resolved

def fin():
print("Clean up:")
delete_items_from_yaml(kube_apis, src_syslog_yaml, test_namespace)
delete_items_from_yaml(kube_apis, src_ing_yaml, test_namespace)
delete_ap_policy(kube_apis.custom_objects, pol_name, test_namespace)
delete_ap_logconf(kube_apis.custom_objects, log_name, test_namespace)
delete_common_app(kube_apis, "grpc", test_namespace)
delete_items_from_yaml(kube_apis, src_sec_yaml, test_namespace)
replace_configmap_from_yaml(kube_apis.v1,
ingress_controller_prerequisites.config_map['metadata']['name'],
ingress_controller_prerequisites.namespace,
f"{DEPLOYMENTS}/common/nginx-config.yaml")

request.addfinalizer(fin)

return BackendSetup(ingress_host, ingress_controller_endpoint.port_ssl)


@pytest.mark.skip_for_nginx_oss
@pytest.mark.appprotect
@pytest.mark.smoke
@pytest.mark.parametrize(
"crd_ingress_controller_with_ap",
[{"extra_args": [f"-enable-custom-resources", f"-enable-app-protect"]}],
indirect=["crd_ingress_controller_with_ap"],
)
class TestAppProtect:
@pytest.mark.parametrize("backend_setup", [{"policy": "grpc-block-sayhello"}], indirect=True)
vepatel marked this conversation as resolved.
Show resolved Hide resolved
def test_responses_grpc_block(
self, kube_apis, crd_ingress_controller_with_ap, backend_setup, test_namespace
):
"""
Test grpc-block-hello AppProtect policy: Blocks /sayhello gRPC method only
Client sends request to /sayhello
"""
syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name
block_response = subprocess.run(["binaries/./grpc_client", "-address", f"{backend_setup.ingress_host}:{backend_setup.ssl_port}"], capture_output=True)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

would it make sense to use https://github.com/grpc/grpc/blob/v1.37.1/examples/python/helloworld/greeter_client.py ? (providing we can make it work). this way there will be no need to add any binaries to the repo.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm checking https://pypi.org/project/grpc-requests/ which is based on the same, will update if it works!

stdout = (block_response.stderr).decode("ascii")
print(stdout)
log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace)
print(log_contents)
assert (
valid_resp_txt not in stdout and
invalid_resp_text in stdout and
'ASM:attack_type="Directory Indexing"' in log_contents and
'violations="Illegal gRPC method"' in log_contents and
'severity="Error"' in log_contents and
'outcome="REJECTED"' in log_contents
)

@pytest.mark.parametrize("backend_setup", [{"policy": "grpc-block-saygoodbye"}], indirect=True)
def test_responses_grpc_allow(
self, kube_apis, crd_ingress_controller_with_ap, backend_setup, test_namespace
):
"""
Test grpc-block-saygoodbye AppProtect policy: Blocks /saygoodbye gRPC method only
Client sends request to /sayhello
"""
syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name
allow_response = subprocess.run(["binaries/./grpc_client", "-address", f"{backend_setup.ingress_host}:{backend_setup.ssl_port}"], capture_output=True)
stdout = (allow_response.stderr).decode("ascii")
print(stdout)
log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace)
print(log_contents)
assert (
valid_resp_txt in stdout and
invalid_resp_text not in stdout and
'ASM:attack_type="N/A"' in log_contents and
'violations="N/A"' in log_contents and
'severity="Informational"' in log_contents and
'outcome="PASSED"' in log_contents
)