From 65adfd19c086b6d073e4dc906fe30fb641a65fe6 Mon Sep 17 00:00:00 2001 From: Venktesh Shivam Patel <64591391+vepatel@users.noreply.github.com> Date: Thu, 3 Jun 2021 11:12:19 +0100 Subject: [PATCH] AppProtect gRPC automation tests (#1603) * Add AppProtect gRPC tests Co-authored-by: Michael Pleshakov --- tests/data/appprotect/appprotect-secret.yaml | 4 +- .../grpc/grpc-block-saygoodbye.yaml | 32 +++ .../appprotect/grpc/grpc-block-sayhello.yaml | 32 +++ tests/data/appprotect/grpc/ingress.yaml | 25 +++ tests/data/appprotect/grpc/nginx-config.yaml | 7 + tests/data/common/app/grpc/app.yaml | 32 +++ tests/requirements.txt | 4 +- tests/suite/grpc/helloworld_pb2.py | 134 ++++++++++++ tests/suite/grpc/helloworld_pb2_grpc.py | 48 +++++ tests/suite/test_app_protect_grpc.py | 198 ++++++++++++++++++ 10 files changed, 513 insertions(+), 3 deletions(-) create mode 100644 tests/data/appprotect/grpc/grpc-block-saygoodbye.yaml create mode 100644 tests/data/appprotect/grpc/grpc-block-sayhello.yaml create mode 100644 tests/data/appprotect/grpc/ingress.yaml create mode 100644 tests/data/appprotect/grpc/nginx-config.yaml create mode 100644 tests/data/common/app/grpc/app.yaml create mode 100644 tests/suite/grpc/helloworld_pb2.py create mode 100644 tests/suite/grpc/helloworld_pb2_grpc.py create mode 100644 tests/suite/test_app_protect_grpc.py diff --git a/tests/data/appprotect/appprotect-secret.yaml b/tests/data/appprotect/appprotect-secret.yaml index ed65956585..c6c15f2d3c 100644 --- a/tests/data/appprotect/appprotect-secret.yaml +++ b/tests/data/appprotect/appprotect-secret.yaml @@ -4,5 +4,5 @@ metadata: name: appprotect-secret type: kubernetes.io/tls data: - tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURMakNDQWhZQ0NRREFPRjl0THNhWFdqQU5CZ2txaGtpRzl3MEJBUXNGQURCYU1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhJVEFmQmdOVkJBb01HRWx1ZEdWeWJtVjBJRmRwWkdkcGRITWdVSFI1SUV4MApaREViTUJrR0ExVUVBd3dTWTJGbVpTNWxlR0Z0Y0d4bExtTnZiU0FnTUI0WERURTRNRGt4TWpFMk1UVXpOVm9YCkRUSXpNRGt4TVRFMk1UVXpOVm93V0RFTE1Ba0dBMVVFQmhNQ1ZWTXhDekFKQmdOVkJBZ01Ba05CTVNFd0h3WUQKVlFRS0RCaEpiblJsY201bGRDQlhhV1JuYVhSeklGQjBlU0JNZEdReEdUQVhCZ05WQkFNTUVHTmhabVV1WlhoaApiWEJzWlM1amIyMHdnZ0VpTUEwR0NTcUdTSWIzRFFFQkFRVUFBNElCRHdBd2dnRUtBb0lCQVFDcDZLbjdzeTgxCnAwanVKL2N5ayt2Q0FtbHNmanRGTTJtdVpOSzBLdGVjcUcyZmpXUWI1NXhRMVlGQTJYT1N3SEFZdlNkd0kyaloKcnVXOHFYWENMMnJiNENaQ0Z4d3BWRUNyY3hkam0zdGVWaVJYVnNZSW1tSkhQUFN5UWdwaW9iczl4N0RsTGM2SQpCQTBaalVPeWwwUHFHOVNKZXhNVjczV0lJYTVyRFZTRjJyNGtTa2JBajREY2o3TFhlRmxWWEgySTVYd1hDcHRDCm42N0pDZzQyZitrOHdnemNSVnA4WFprWldaVmp3cTlSVUtEWG1GQjJZeU4xWEVXZFowZXdSdUtZVUpsc202OTIKc2tPcktRajB2a29QbjQxRUUvK1RhVkVwcUxUUm9VWTNyemc3RGtkemZkQml6Rk8yZHNQTkZ4MkNXMGpYa05MdgpLbzI1Q1pyT2hYQUhBZ01CQUFFd0RRWUpLb1pJaHZjTkFRRUxCUUFEZ2dFQkFLSEZDY3lPalp2b0hzd1VCTWRMClJkSEliMzgzcFdGeW5acS9MdVVvdnNWQTU4QjBDZzdCRWZ5NXZXVlZycTVSSWt2NGxaODFOMjl4MjFkMUpINnIKalNuUXgrRFhDTy9USkVWNWxTQ1VwSUd6RVVZYVVQZ1J5anNNL05VZENKOHVIVmhaSitTNkZBK0NuT0Q5cm4yaQpaQmVQQ0k1ckh3RVh3bm5sOHl3aWozdnZRNXpISXV5QmdsV3IvUXl1aTlmalBwd1dVdlVtNG52NVNNRzl6Q1Y3ClBwdXd2dWF0cWpPMTIwOEJqZkUvY1pISWc4SHc5bXZXOXg5QytJUU1JTURFN2IvZzZPY0s3TEdUTHdsRnh2QTgKN1dqRWVxdW5heUlwaE1oS1JYVmYxTjM0OWVOOThFejM4Zk9USFRQYmRKakZBL1BjQytHeW1lK2lHdDVPUWRGaAp5UkU9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K - tls.key: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb3dJQkFBS0NBUUVBcWVpcCs3TXZOYWRJN2lmM01wUHJ3Z0pwYkg0N1JUTnBybVRTdENyWG5LaHRuNDFrCkcrZWNVTldCUU5semtzQndHTDBuY0NObzJhN2x2S2wxd2k5cTIrQW1RaGNjS1ZSQXEzTVhZNXQ3WGxZa1YxYkcKQ0pwaVJ6ejBza0lLWXFHN1BjZXc1UzNPaUFRTkdZMURzcGRENmh2VWlYc1RGZTkxaUNHdWF3MVVoZHErSkVwRwp3SStBM0kreTEzaFpWVng5aU9WOEZ3cWJRcCt1eVFvT05uL3BQTUlNM0VWYWZGMlpHVm1WWThLdlVWQ2cxNWhRCmRtTWpkVnhGbldkSHNFYmltRkNaYkp1dmRySkRxeWtJOUw1S0Q1K05SQlAvazJsUkthaTAwYUZHTjY4NE93NUgKYzMzUVlzeFR0bmJEelJjZGdsdEkxNURTN3lxTnVRbWF6b1Z3QndJREFRQUJBb0lCQVFDUFNkU1luUXRTUHlxbApGZlZGcFRPc29PWVJoZjhzSStpYkZ4SU91UmF1V2VoaEp4ZG01Uk9ScEF6bUNMeUw1VmhqdEptZTIyM2dMcncyCk45OUVqVUtiL1ZPbVp1RHNCYzZvQ0Y2UU5SNThkejhjbk9SVGV3Y290c0pSMXBuMWhobG5SNUhxSkpCSmFzazEKWkVuVVFmY1hackw5NGxvOUpIM0UrVXFqbzFGRnM4eHhFOHdvUEJxalpzVjdwUlVaZ0MzTGh4bndMU0V4eUZvNApjeGI5U09HNU9tQUpvelN0Rm9RMkdKT2VzOHJKNXFmZHZ5dGdnOXhiTGFRTC94MGtwUTYyQm9GTUJEZHFPZVBXCktmUDV6WjYvMDcvdnBqNDh5QTFRMzJQem9idWJzQkxkM0tjbjMyamZtMUU3cHJ0V2wrSmVPRmlPem5CUUZKYk4KNHFQVlJ6NWhBb0dCQU50V3l4aE5DU0x1NFArWGdLeWNrbGpKNkY1NjY4Zk5qNUN6Z0ZScUowOXpuMFRsc05ybwpGVExaY3hEcW5SM0hQWU00MkpFUmgySi9xREZaeW5SUW8zY2czb2VpdlVkQlZHWTgrRkkxVzBxZHViL0w5K3l1CmVkT1pUUTVYbUdHcDZyNmpleHltY0ppbS9Pc0IzWm5ZT3BPcmxEN1NQbUJ2ek5MazRNRjZneGJYQW9HQkFNWk8KMHA2SGJCbWNQMHRqRlhmY0tFNzdJbUxtMHNBRzR1SG9VeDBlUGovMnFyblRuT0JCTkU0TXZnRHVUSnp5K2NhVQprOFJxbWRIQ2JIelRlNmZ6WXEvOWl0OHNaNzdLVk4xcWtiSWN1YytSVHhBOW5OaDFUanNSbmU3NFowajFGQ0xrCmhIY3FIMHJpN1BZU0tIVEU4RnZGQ3haWWRidUI4NENtWmlodnhicFJBb0dBSWJqcWFNWVBUWXVrbENkYTVTNzkKWVNGSjFKelplMUtqYS8vdER3MXpGY2dWQ0thMzFqQXdjaXowZi9sU1JxM0hTMUdHR21lemhQVlRpcUxmZVpxYwpSMGlLYmhnYk9jVlZrSkozSzB5QXlLd1BUdW14S0haNnpJbVpTMGMwYW0rUlk5WUdxNVQ3WXJ6cHpjZnZwaU9VCmZmZTNSeUZUN2NmQ21mb09oREN0enVrQ2dZQjMwb0xDMVJMRk9ycW40M3ZDUzUxemM1em9ZNDR1QnpzcHd3WU4KVHd2UC9FeFdNZjNWSnJEakJDSCtULzZzeXNlUGJKRUltbHpNK0l3eXRGcEFOZmlJWEV0LzQ4WGY2ME54OGdXTQp1SHl4Wlp4L05LdER3MFY4dlgxUE9ucTJBNWVpS2ErOGpSQVJZS0pMWU5kZkR1d29seHZHNmJaaGtQaS80RXRUCjNZMThzUUtCZ0h0S2JrKzdsTkpWZXN3WEU1Y1VHNkVEVXNEZS8yVWE3ZlhwN0ZjanFCRW9hcDFMU3crNlRYcDAKWmdybUtFOEFSek00NytFSkhVdmlpcS9udXBFMTVnMGtKVzNzeWhwVTl6WkxPN2x0QjBLSWtPOVpSY21Vam84UQpjcExsSE1BcWJMSjhXWUdKQ2toaVd4eWFsNmhZVHlXWTRjVmtDMHh0VGwvaFVFOUllTktvCi0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJakNDQWdvQ0NRQ0pqcjBWaG5mTFpEQU5CZ2txaGtpRzl3MEJBUXNGQURCVE1Rc3dDUVlEVlFRR0V3SlYKVXpFTE1Ba0dBMVVFQ0F3Q1EwRXhGakFVQmdOVkJBY01EVk5oYmlCR2NtRnVZMmx6WTI4eEh6QWRCZ05WQkFNTQpGbUZ3Y0hCeWIzUmxZM1F1WlhoaGJYQnNaUzVqYjIwd0hoY05NakV3TmpBeU1ERTFOVEkxV2hjTk16RXdOVE14Ck1ERTFOVEkxV2pCVE1Rc3dDUVlEVlFRR0V3SlZVekVMTUFrR0ExVUVDQXdDUTBFeEZqQVVCZ05WQkFjTURWTmgKYmlCR2NtRnVZMmx6WTI4eEh6QWRCZ05WQkFNTUZtRndjSEJ5YjNSbFkzUXVaWGhoYlhCc1pTNWpiMjB3Z2dFaQpNQTBHQ1NxR1NJYjNEUUVCQVFVQUE0SUJEd0F3Z2dFS0FvSUJBUURsc2dHc3pXcDNwRllyY0NNZlpTSDBsYnZ5Cnk5MWJJYjhLQ3JhUG5vUTBGdjZneFNmWGtlTVI3dHRHRVI2TTZJdGVKR3hsQTNDS3pESUFPWEtjMHFvTER0Z2YKSUdEQmdWa3dIbGZSa1pld1dIdW51MVU3UmxqTVpvMGVsQUQ0YzVtVDVIV1EvRDF5YmJIR2dCQ2JLR0tZQlM1LwpISXdlb05zZVJpREY3SVpVRUxyak8rdUVHMHA0TnJublAvN1V4aGwrZEd1VXN2ZkdiZERzTnBrYUpzcDlyRDZMCjNrRVJvdkpveDJaV0Q2alkxdisyeGpqTVlCaTFlV2xTQkFHSWZzYVgyUmhXRFM0V0lSS2xBY2txSmgxMkJPTGkKMU12SnJtSEdRVXVCK3lRakc0KzdUdkhPQ1U3OVZHYkRwdnRBd280d2dMV1RNWmRzNG1UNTJoVGFXUXlGQWdNQgpBQUV3RFFZSktvWklodmNOQVFFTEJRQURnZ0VCQU1EUmlxSzJtbGtLSTdKbFVZRjFHRzNveForZmpaUkdPU2s5CkJnWTNieXBUREQwU2QwVHJDUlZPN1ZXS3JhaU5uK2tDQkgwTEFsRHo0QWZ1QTJTRzhVZzJSVVpXcjhwSVBxRGkKdXhmZEl2SjFaTW9oRjlzWnhPL3d1K0tlbVg5M0hzTHlyMlNwQkx5YllYYlRKOFQ4WVp4L2FEZWpBOG44N2g5UAoxNUV4TTV0NFlZZ1h6NkdSRHJvRDVWbkVRbGRna29YbVk1WlQyT3hlb0JWM0FDcVNrOFZIdCswd05TTnd1dEVZCnRzSVQzY25VK0lpWFR3MHNpWTdaMnpscEFyYXYybjFLT2JkZjhkU1BvdFNGbjhFa045OEdoT1RWNG84QitudUQKMTJlb3pOM2VtMm5IakdWbzZiaDI0RnhNa0NGZ1RoVUhtU0RwaGFETVdjQkFmb015RDYwPQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRRGxzZ0dzeldwM3BGWXIKY0NNZlpTSDBsYnZ5eTkxYkliOEtDcmFQbm9RMEZ2Nmd4U2ZYa2VNUjd0dEdFUjZNNkl0ZUpHeGxBM0NLekRJQQpPWEtjMHFvTER0Z2ZJR0RCZ1Zrd0hsZlJrWmV3V0h1bnUxVTdSbGpNWm8wZWxBRDRjNW1UNUhXUS9EMXliYkhHCmdCQ2JLR0tZQlM1L0hJd2VvTnNlUmlERjdJWlVFTHJqTyt1RUcwcDROcm5uUC83VXhobCtkR3VVc3ZmR2JkRHMKTnBrYUpzcDlyRDZMM2tFUm92Sm94MlpXRDZqWTF2KzJ4ampNWUJpMWVXbFNCQUdJZnNhWDJSaFdEUzRXSVJLbApBY2txSmgxMkJPTGkxTXZKcm1IR1FVdUIreVFqRzQrN1R2SE9DVTc5VkdiRHB2dEF3bzR3Z0xXVE1aZHM0bVQ1CjJoVGFXUXlGQWdNQkFBRUNnZ0VBUE1VaXNsQkttY3JOellKR21Kak9LZ0t2amlZbnMyT3pRbm5oSVJCRVUrKzkKZ0ZXSkcveUtBZ1dhMStmUm1HQXg5ejlWdWtmMXI1TWtxM0NIaFR3ckp1L1BlRFM5eEpSdlAwN1gxeWRZNVp3VApZeVlwLzV1MkJLWWhNZlRnYU50VXg0OExGOGZVamdDOXB4SGMwdUFWYmJqNU8wSVhSRWMwa2NibUJ0ZDBGcXN5Ckh4NjUwYldiSS9WUDVmWWE3ejh2OW5WcmZFMlFkaWczWnVvZUcrZTNWOHY2dXljVmFzZ1pPb2Z1OC9XQXZwSlEKUWdXQW1LNVJqR2sxZkdWMHF5NGY0S0RnK1R4cUV0QWN6bnZMN1V4UmNrT3NiQlpYUWtEM1BOUG43OW1aT016RApxbnhWcTFoOURRVDY5a1pVd3pkUEpZLytRaWc4VDNwUmMwQ0ZoQURNZ1FLQmdRRDN3YkZiRHROQ1VNc0IxUTRvCktuYTNCYzdLT0tiUHVuN0dxbDhFZWhXUllyZm1WdXhYWTNSSTZLMXhFZlMzcjNiRmlCYlZFWi9ybFFrM2kzK3UKaHVtOXRCRHFoZHVpZGEyVU5LRnZ2ZXR3OUdSMkJaNFVXTVpSUDFucCtJY0hqQVRaYXIvRG5hZk9tazhQSzZ6bAo0V3MvdXlONzFXSCthOUVPUWpNMURuVzJwUUtCZ1FEdFZubFNBMWFUdHF4UXZ1aWJ6eVUrYzR3eEswQVo0SHlqCklEc01TbGtFeVpWRXplKzc2YWtTMlpKQVZIRlZPOURXZ0wvTWFBcXdWNHJubnFPYjZ3NERianM2cEU3Wkl2TnkKcGU3R2cvdER0dnZETWZNODlpYmxCUElIbEwyTWU5VjJIWWlVRlRaSHZqaGFlOVRrTXVuMHVPakVQbjE4WlIzbgpnT1FlMFdQNFlRS0JnQWpUeHR6bXIxSTZqTEVaQlRNYktEV25LRkgrS0x0WCtySmJXWjFRT0RxQTJPcUZDRkNvCnczamhpV1J3N2xZcEUwTkFjUWZRWS9GQjB4MCtoQ01VdlhrNzFDcDI4SGRlVi92aTBDbXFDNXh2cUxDbnpKVVAKQUtuZVp2YTJHeUdDdEoyR2R5U2lGbHQvRmZnRloxaWRhblBQMkJqT1pucHBIdjZ5RHg4QnorSGRBb0dBTkFPKwpwUDI4VExVOVFKa0dhNWRUWjBOMWx4VU9nRjFWazM2dDBqeTlFN0tHTjBVVzVFMEtVK3BCQmo0N2RGMmVvRlFwCk5NZ2NoNWM0QnhrYWJFSndtTW5neXpKVllYc3FkWmJ1SksycU5LWDJ1VW5LTURNV1JSejNXamY3N3J6NUJHRG0Kbk9XbE5zWDJuY1lEMjF0Wk1Od1JqMmpPb2FPYmtUQUxUc0cvTWlFQ2dZRUF5aGFEVFh5RnNicEF3bU5yejRuawpoVjVDREVSdnp6MW5NRHp6bTdselhEQkpKZit3Q1VmUnhmY2pSRlV2RWV1cmJ5d2RncForeHBHSkhheE1Pd1FlCk5Bd2RvSjFtbDhHVGozRjYrTUpnZU5CdTgza09abXpXSzQ2MW54NGJhUmplVlBMWTN2WFgvZmVMNi92UnNjQmMKWnpaWkthTVI1REhtU3cwZ3MwSFN1VWc9Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K \ No newline at end of file diff --git a/tests/data/appprotect/grpc/grpc-block-saygoodbye.yaml b/tests/data/appprotect/grpc/grpc-block-saygoodbye.yaml new file mode 100644 index 0000000000..852a7f4c24 --- /dev/null +++ b/tests/data/appprotect/grpc/grpc-block-saygoodbye.yaml @@ -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 diff --git a/tests/data/appprotect/grpc/grpc-block-sayhello.yaml b/tests/data/appprotect/grpc/grpc-block-sayhello.yaml new file mode 100644 index 0000000000..c5833d5afe --- /dev/null +++ b/tests/data/appprotect/grpc/grpc-block-sayhello.yaml @@ -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 diff --git a/tests/data/appprotect/grpc/ingress.yaml b/tests/data/appprotect/grpc/ingress.yaml new file mode 100644 index 0000000000..4d87ada5cb --- /dev/null +++ b/tests/data/appprotect/grpc/ingress.yaml @@ -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 diff --git a/tests/data/appprotect/grpc/nginx-config.yaml b/tests/data/appprotect/grpc/nginx-config.yaml new file mode 100644 index 0000000000..1bfa98299f --- /dev/null +++ b/tests/data/appprotect/grpc/nginx-config.yaml @@ -0,0 +1,7 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: nginx-config + namespace: nginx-ingress +data: + http2: "True" diff --git a/tests/data/common/app/grpc/app.yaml b/tests/data/common/app/grpc/app.yaml new file mode 100644 index 0000000000..484063bf6a --- /dev/null +++ b/tests/data/common/app/grpc/app.yaml @@ -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 + ports: + - containerPort: 50051 +--- +apiVersion: v1 +kind: Service +metadata: + name: grpc-svc +spec: + ports: + - port: 50051 + targetPort: 50051 + protocol: TCP + name: grpc + selector: + app: greeter \ No newline at end of file diff --git a/tests/requirements.txt b/tests/requirements.txt index e94e591a8f..268372e4a6 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -11,4 +11,6 @@ urllib3==1.26.5 pytest-html==3.1.1 pytest-profiling==1.7.0 more-itertools==8.8.0 -mock==4.0.3 \ No newline at end of file +mock==4.0.3 +grpcio==1.38.0 +grpcio-tools==1.38.0 diff --git a/tests/suite/grpc/helloworld_pb2.py b/tests/suite/grpc/helloworld_pb2.py new file mode 100644 index 0000000000..1bb6ce053a --- /dev/null +++ b/tests/suite/grpc/helloworld_pb2.py @@ -0,0 +1,134 @@ +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: helloworld.proto + +import sys +_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1')) +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from google.protobuf import reflection as _reflection +from google.protobuf import symbol_database as _symbol_database +from google.protobuf import descriptor_pb2 +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor.FileDescriptor( + name='helloworld.proto', + package='helloworld', + syntax='proto3', + serialized_pb=_b('\n\x10helloworld.proto\x12\nhelloworld\"\x1c\n\x0cHelloRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1d\n\nHelloReply\x12\x0f\n\x07message\x18\x01 \x01(\t2I\n\x07Greeter\x12>\n\x08SayHello\x12\x18.helloworld.HelloRequest\x1a\x16.helloworld.HelloReply\"\x00\x42\x36\n\x1bio.grpc.examples.helloworldB\x0fHelloWorldProtoP\x01\xa2\x02\x03HLWb\x06proto3') +) + + + + +_HELLOREQUEST = _descriptor.Descriptor( + name='HelloRequest', + full_name='helloworld.HelloRequest', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='name', full_name='helloworld.HelloRequest.name', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=32, + serialized_end=60, +) + + +_HELLOREPLY = _descriptor.Descriptor( + name='HelloReply', + full_name='helloworld.HelloReply', + filename=None, + file=DESCRIPTOR, + containing_type=None, + fields=[ + _descriptor.FieldDescriptor( + name='message', full_name='helloworld.HelloReply.message', index=0, + number=1, type=9, cpp_type=9, label=1, + has_default_value=False, default_value=_b("").decode('utf-8'), + message_type=None, enum_type=None, containing_type=None, + is_extension=False, extension_scope=None, + options=None), + ], + extensions=[ + ], + nested_types=[], + enum_types=[ + ], + options=None, + is_extendable=False, + syntax='proto3', + extension_ranges=[], + oneofs=[ + ], + serialized_start=62, + serialized_end=91, +) + +DESCRIPTOR.message_types_by_name['HelloRequest'] = _HELLOREQUEST +DESCRIPTOR.message_types_by_name['HelloReply'] = _HELLOREPLY +_sym_db.RegisterFileDescriptor(DESCRIPTOR) + +HelloRequest = _reflection.GeneratedProtocolMessageType('HelloRequest', (_message.Message,), dict( + DESCRIPTOR = _HELLOREQUEST, + __module__ = 'helloworld_pb2' + # @@protoc_insertion_point(class_scope:helloworld.HelloRequest) + )) +_sym_db.RegisterMessage(HelloRequest) + +HelloReply = _reflection.GeneratedProtocolMessageType('HelloReply', (_message.Message,), dict( + DESCRIPTOR = _HELLOREPLY, + __module__ = 'helloworld_pb2' + # @@protoc_insertion_point(class_scope:helloworld.HelloReply) + )) +_sym_db.RegisterMessage(HelloReply) + + +DESCRIPTOR.has_options = True +DESCRIPTOR._options = _descriptor._ParseOptions(descriptor_pb2.FileOptions(), _b('\n\033io.grpc.examples.helloworldB\017HelloWorldProtoP\001\242\002\003HLW')) + +_GREETER = _descriptor.ServiceDescriptor( + name='Greeter', + full_name='helloworld.Greeter', + file=DESCRIPTOR, + index=0, + options=None, + serialized_start=93, + serialized_end=166, + methods=[ + _descriptor.MethodDescriptor( + name='SayHello', + full_name='helloworld.Greeter.SayHello', + index=0, + containing_service=None, + input_type=_HELLOREQUEST, + output_type=_HELLOREPLY, + options=None, + ), +]) +_sym_db.RegisterServiceDescriptor(_GREETER) + +DESCRIPTOR.services_by_name['Greeter'] = _GREETER + +# @@protoc_insertion_point(module_scope) \ No newline at end of file diff --git a/tests/suite/grpc/helloworld_pb2_grpc.py b/tests/suite/grpc/helloworld_pb2_grpc.py new file mode 100644 index 0000000000..46b26e5c02 --- /dev/null +++ b/tests/suite/grpc/helloworld_pb2_grpc.py @@ -0,0 +1,48 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +import grpc + +from suite.grpc.helloworld_pb2 import ( + HelloRequest, + HelloReply, +) + + +class GreeterStub(object): + """The greeting service definition. + """ + + def __init__(self, channel): + """Constructor. + Args: + channel: A grpc.Channel. + """ + self.SayHello = channel.unary_unary( + '/helloworld.Greeter/SayHello', + request_serializer=HelloRequest.SerializeToString, + response_deserializer=HelloReply.FromString, + ) + + +class GreeterServicer(object): + """The greeting service definition. + """ + + def SayHello(self, request, context): + """Sends a greeting + """ + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_GreeterServicer_to_server(servicer, server): + rpc_method_handlers = { + 'SayHello': grpc.unary_unary_rpc_method_handler( + servicer.SayHello, + request_deserializer=HelloRequest.FromString, + response_serializer=HelloReply.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'helloworld.Greeter', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) \ No newline at end of file diff --git a/tests/suite/test_app_protect_grpc.py b/tests/suite/test_app_protect_grpc.py new file mode 100644 index 0000000000..a00067f86b --- /dev/null +++ b/tests/suite/test_app_protect_grpc.py @@ -0,0 +1,198 @@ +import grpc +import pytest +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.grpc.helloworld_pb2 import HelloRequest +from suite.grpc.helloworld_pb2_grpc import GreeterStub + +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.ssl_utils import get_certificate +from suite.yaml_utils import get_first_ingress_host_from_yaml + +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, ip, port_ssl): + self.ingress_host = ingress_host + self.ip = ip + self.port_ssl = port_ssl + + +@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 -----------------------------") + 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) + wait_before_test(40) + + 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.public_ip, ingress_controller_endpoint.port_ssl) + + +@pytest.mark.skip_for_nginx_oss +@pytest.mark.appprotect +@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.smoke + @pytest.mark.parametrize("backend_setup", [{"policy": "grpc-block-sayhello"}], indirect=True) + 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 + + # we need to get the cert so that it can be used in credentials in grpc.secure_channel to verify itself. + # without verification, we will not be able to use the channel + cert = get_certificate(backend_setup.ip, backend_setup.ingress_host, backend_setup.port_ssl) + + target = f'{backend_setup.ip}:{backend_setup.port_ssl}' + credentials = grpc.ssl_channel_credentials(root_certificates=cert.encode()) + + # this option is necessary to set the SNI of a gRPC connection and it only works with grpc.secure_channel. + # also, the TLS cert for the Ingress must have the CN equal to backend_setup.ingress_host + options = (('grpc.ssl_target_name_override', backend_setup.ingress_host),) + + with grpc.secure_channel(target, credentials, options) as channel: + stub = GreeterStub(channel) + ex = "" + try: + stub.SayHello(HelloRequest(name=backend_setup.ip)) + pytest.fail("RPC error was expected during call, exiting...") + except grpc.RpcError as e: + # grpc.RpcError is also grpc.Call https://grpc.github.io/grpc/python/grpc.html#client-side-context + ex = e.details() + print(ex) + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + assert ( + invalid_resp_text in ex 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, ingress_controller_endpoint + ): + """ + Test grpc-block-goodbye AppProtect policy: Blocks /saygoodbye gRPC method only + Client sends request to /sayhello thus should pass + """ + syslog_pod = kube_apis.v1.list_namespaced_pod(test_namespace).items[-1].metadata.name + cert = get_certificate(backend_setup.ip, backend_setup.ingress_host, backend_setup.port_ssl) + + target = f'{backend_setup.ip}:{backend_setup.port_ssl}' + credentials = grpc.ssl_channel_credentials(root_certificates=cert.encode()) + options = (('grpc.ssl_target_name_override', backend_setup.ingress_host),) + + with grpc.secure_channel(target, credentials, options) as channel: + stub = GreeterStub(channel) + response = "" + try: + response = stub.SayHello(HelloRequest(name=backend_setup.ip)) + print(response) + except grpc.RpcError as e: + print(e.details()) + pytest.fail("RPC error was not expected during call, exiting...") + + log_contents = get_file_contents(kube_apis.v1, log_loc, syslog_pod, test_namespace) + assert ( + valid_resp_txt in response.message 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 + ) \ No newline at end of file