From 6e88d548362b32a15a094fdf8d83f082107c7962 Mon Sep 17 00:00:00 2001 From: Michal Jagiello Date: Wed, 19 Apr 2023 09:53:38 +0000 Subject: Fix security versions script That script was usused on security versions tests, so I updated it with the latest changes from repo which was really used, created needed files and after we merge it we could use that on security tests. Issue-ID: TEST-394 Signed-off-by: Michal Jagiello Change-Id: I8e5daa7d43e2723bbe3308cf85b1cae2b2f587ad --- test/security/check_versions/.gitignore | 1 - test/security/check_versions/README.md | 15 +- test/security/check_versions/env/Vagrantfile | 36 - .../env/configuration/namespaces.yaml | 45 -- .../env/configuration/terminated.yaml | 17 - .../check_versions/env/configuration/versions.yaml | 112 --- .../check_versions/env/requirements-dev.txt | 9 - test/security/check_versions/env/requirements.txt | 6 - test/security/check_versions/pyproject.toml | 21 + test/security/check_versions/requirements.txt | 7 + .../src/k8s_bin_versions_inspector.py | 745 -------------------- test/security/check_versions/tests/test_main.py | 4 - .../tests/test_verify_versions_acceptability.py | 4 - test/security/check_versions/tox.ini | 10 +- test/security/check_versions/versions/__init__.py | 0 .../versions/k8s_bin_versions_inspector.py | 769 +++++++++++++++++++++ .../k8s_bin_versions_inspector_test_case.py | 116 ++++ test/security/check_versions/versions/reporting.py | 265 +++++++ .../check_versions/versions/templates/base.html.j2 | 232 +++++++ .../versions/templates/versions.html.j2 | 85 +++ 20 files changed, 1502 insertions(+), 997 deletions(-) delete mode 100644 test/security/check_versions/env/Vagrantfile delete mode 100644 test/security/check_versions/env/configuration/namespaces.yaml delete mode 100644 test/security/check_versions/env/configuration/terminated.yaml delete mode 100644 test/security/check_versions/env/configuration/versions.yaml delete mode 100644 test/security/check_versions/env/requirements-dev.txt delete mode 100644 test/security/check_versions/env/requirements.txt create mode 100644 test/security/check_versions/pyproject.toml create mode 100644 test/security/check_versions/requirements.txt delete mode 100644 test/security/check_versions/src/k8s_bin_versions_inspector.py create mode 100644 test/security/check_versions/versions/__init__.py create mode 100644 test/security/check_versions/versions/k8s_bin_versions_inspector.py create mode 100644 test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py create mode 100644 test/security/check_versions/versions/reporting.py create mode 100644 test/security/check_versions/versions/templates/base.html.j2 create mode 100644 test/security/check_versions/versions/templates/versions.html.j2 diff --git a/test/security/check_versions/.gitignore b/test/security/check_versions/.gitignore index db6444b3c..2b574f8c0 100644 --- a/test/security/check_versions/.gitignore +++ b/test/security/check_versions/.gitignore @@ -1,5 +1,4 @@ .pytest_cache/ __pycache__/ -/env/.vagrant /temp/ /.tox/ diff --git a/test/security/check_versions/README.md b/test/security/check_versions/README.md index 3934ca77a..399d10443 100644 --- a/test/security/check_versions/README.md +++ b/test/security/check_versions/README.md @@ -6,25 +6,12 @@ in the kubernetes cluster containers. ## Commands -### Creating environment - -All development and testing process, should be done in prepared virtual machine, -that is containing development environment for this project. Vagrant plugins, -that are required to start virtual machine: `vagrant-libvirt`, `vagrant-reload`, -`vagrant-sshfs`. - -```bash -cd env -vagrant up -vagrant ssh -``` - ### Install dependencies To install dependencies for normal usage of script, run this command. ```bash -pip3 install -r env/requirements.txt +pip3 install -r requirements.txt ``` ### Code formatting diff --git a/test/security/check_versions/env/Vagrantfile b/test/security/check_versions/env/Vagrantfile deleted file mode 100644 index 9753a74ad..000000000 --- a/test/security/check_versions/env/Vagrantfile +++ /dev/null @@ -1,36 +0,0 @@ -# -*- mode: ruby -*- -# vi: set ft=ruby : - -Vagrant.configure("2") do |config| - - config.vm.provider :libvirt do |libvirt| - libvirt.default_prefix = "k8s_bin_versions_inspector"; - libvirt.driver = "kvm"; - libvirt.cpus = 6; - libvirt.memory = 12288; - end - - config.vm.box = "generic/ubuntu1804"; - config.vm.hostname = "k8s-bin-versions-inspector"; - config.vm.synced_folder ".", "/vagrant", disabled: true; - config.vm.synced_folder "..", "/home/vagrant/k8s_bin_versions_inspector", type: :sshfs; - - config.vm.provision "shell", inline: <<-end - export DEBIAN_FRONTEND=noninteractive &&\ - apt-get update &&\ - apt-get upgrade -y &&\ - apt-get dist-upgrade -y &&\ - apt-get install -y python3 python3-pip snap git vim net-tools htop &&\ - pip3 install --system -r /home/vagrant/k8s_bin_versions_inspector/env/requirements-dev.txt &&\ - snap install --classic microk8s &&\ - usermod -a -G microk8s vagrant - end - config.vm.provision :reload; - config.vm.provision "shell", privileged: false, inline: <<-end - microk8s start &&\ - microk8s status --wait-ready &&\ - microk8s config > /home/vagrant/.kube/config &&\ - microk8s kubectl apply -f /home/vagrant/k8s_bin_versions_inspector/env/configuration - end -end - diff --git a/test/security/check_versions/env/configuration/namespaces.yaml b/test/security/check_versions/env/configuration/namespaces.yaml deleted file mode 100644 index f300cc7da..000000000 --- a/test/security/check_versions/env/configuration/namespaces.yaml +++ /dev/null @@ -1,45 +0,0 @@ ---- -apiVersion: v1 -kind: Namespace -metadata: - name: ingress-nginx - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-ingress-nginx - namespace: ingress-nginx -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-ingress-nginx - template: - metadata: - labels: - app: kbvi-test-ingress-nginx - spec: - containers: - - name: echo-server - image: jmalloc/echo-server - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-kube-system - namespace: kube-system -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-kube-system - template: - metadata: - labels: - app: kbvi-test-kube-system - spec: - containers: - - name: echo-server - image: jmalloc/echo-server diff --git a/test/security/check_versions/env/configuration/terminated.yaml b/test/security/check_versions/env/configuration/terminated.yaml deleted file mode 100644 index dd6ce829d..000000000 --- a/test/security/check_versions/env/configuration/terminated.yaml +++ /dev/null @@ -1,17 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-terminated -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-terminated - template: - metadata: - labels: - app: kbvi-test-terminated - spec: - containers: - - name: python - image: python diff --git a/test/security/check_versions/env/configuration/versions.yaml b/test/security/check_versions/env/configuration/versions.yaml deleted file mode 100644 index 75b7f7b85..000000000 --- a/test/security/check_versions/env/configuration/versions.yaml +++ /dev/null @@ -1,112 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-jupyter -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-jupyter - template: - metadata: - labels: - app: kbvi-test-python-jupyter - spec: - containers: - - name: jupyter - image: jupyter/base-notebook - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-jupyter-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-jupyter-old - template: - metadata: - labels: - app: kbvi-test-python-jupyter-old - spec: - containers: - - name: jupyter-old - image: jupyter/base-notebook:ff922f8f533a - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-python-stderr-filebeat -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-python-stderr-filebeat - template: - metadata: - labels: - app: kbvi-test-python-stderr-filebeat - spec: - containers: - - name: filebeat - image: docker.elastic.co/beats/filebeat:5.5.0 - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak - template: - metadata: - labels: - app: kbvi-test-java-keycloak - spec: - containers: - - name: keycloak - image: jboss/keycloak - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak-old - template: - metadata: - labels: - app: kbvi-test-java-keycloak-old - spec: - containers: - - name: keycloak-old - image: jboss/keycloak:8.0.0 - ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: kbvi-test-java-keycloak-very-old -spec: - replicas: 1 - selector: - matchLabels: - app: kbvi-test-java-keycloak-very-old - template: - metadata: - labels: - app: kbvi-test-java-keycloak-very-old - spec: - containers: - - name: keycloak-very-old - image: jboss/keycloak:2.0.0.Final diff --git a/test/security/check_versions/env/requirements-dev.txt b/test/security/check_versions/env/requirements-dev.txt deleted file mode 100644 index 1ced42c04..000000000 --- a/test/security/check_versions/env/requirements-dev.txt +++ /dev/null @@ -1,9 +0,0 @@ -cerberus -dataclasses -kubernetes -pyyaml -tabulate -black -pylint -pytest - diff --git a/test/security/check_versions/env/requirements.txt b/test/security/check_versions/env/requirements.txt deleted file mode 100644 index e81358f72..000000000 --- a/test/security/check_versions/env/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -cerberus -dataclasses -kubernetes -pyyaml -tabulate - diff --git a/test/security/check_versions/pyproject.toml b/test/security/check_versions/pyproject.toml new file mode 100644 index 000000000..a9b5c540e --- /dev/null +++ b/test/security/check_versions/pyproject.toml @@ -0,0 +1,21 @@ +[project] +name = "check_versions" +readme = "README.md" +version = "1.0" +requires-python = ">=3.7" +dependencies = [ + "kubernetes", + "jinja2", + "xtesting", + "tabulate", + "cerberus", + "packaging", + "wget" +] + +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project.entry-points."xtesting.testcase"] +versions = "versions.k8s_bin_versions_inspector_test_case:Inspector" diff --git a/test/security/check_versions/requirements.txt b/test/security/check_versions/requirements.txt new file mode 100644 index 000000000..8e46a3acf --- /dev/null +++ b/test/security/check_versions/requirements.txt @@ -0,0 +1,7 @@ +kubernetes +jinja2 +xtesting +tabulate +cerberus +packaging +wget diff --git a/test/security/check_versions/src/k8s_bin_versions_inspector.py b/test/security/check_versions/src/k8s_bin_versions_inspector.py deleted file mode 100644 index f5ff53714..000000000 --- a/test/security/check_versions/src/k8s_bin_versions_inspector.py +++ /dev/null @@ -1,745 +0,0 @@ -#!/usr/bin/env python3 - -# COPYRIGHT NOTICE STARTS HERE -# -# Copyright 2020 Samsung Electronics Co., Ltd. -# Copyright 2023 Deutsche Telekom AG -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# COPYRIGHT NOTICE ENDS HERE - -""" -k8s_bin_versions_inspector is a module for verifying versions of CPython and -OpenJDK binaries installed in the kubernetes cluster containers. -""" - -__title__ = "k8s_bin_versions_inspector" -__summary__ = ( - "Module for verifying versions of CPython and OpenJDK binaries installed" - " in the kubernetes cluster containers." -) -__version__ = "0.1.0" -__author__ = "kkkk.k@samsung.com" -__license__ = "Apache-2.0" -__copyright__ = "Copyright 2020 Samsung Electronics Co., Ltd." - -from typing import Iterable, List, Optional, Pattern, Union - -import argparse -import dataclasses -import itertools -import json -import pathlib -import pprint -import re -import string -import sys -import tabulate -import yaml - -import cerberus -import kubernetes - - -def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace: - """Function for parsing command line arguments. - - Args: - argv: Unparsed list of command line arguments. - - Returns: - Namespace with values from parsed arguments. - """ - - epilog = ( - f"Author: {__author__}\n" - f"License: {__license__}\n" - f"Copyright: {__copyright__}\n" - ) - - parser = argparse.ArgumentParser( - formatter_class=argparse.RawTextHelpFormatter, - prog=__title__, - description=__summary__, - epilog=epilog, - add_help=False, - ) - - parser.add_argument("-c", "--config-file", help="Name of the kube-config file.") - - parser.add_argument( - "-s", - "--field-selector", - default="", - help="Kubernetes field selector, to filter out containers objects.", - ) - - parser.add_argument( - "-o", - "--output-file", - type=pathlib.Path, - help="Path to file, where output will be saved.", - ) - - parser.add_argument( - "-f", - "--output-format", - choices=("tabulate", "pprint", "json"), - default="tabulate", - help="Format of the output file (tabulate, pprint, json).", - ) - - parser.add_argument( - "-i", - "--ignore-empty", - action="store_true", - help="Ignore containers without any versions.", - ) - - parser.add_argument( - "-a", - "--acceptable", - type=pathlib.Path, - help="Path to YAML file, with list of acceptable software versions.", - ) - - parser.add_argument( - "-n", - "--namespace", - help="Namespace to use to list pods." - "If empty pods are going to be listed from all namespaces" - ) - - parser.add_argument( - "--check-istio-sidecar", - action="store_true", - help="Add if you want to check istio sidecars also" - ) - - parser.add_argument( - "--istio-sidecar-name", - default="istio-proxy", - help="Name of istio sidecar to filter out" - ) - - parser.add_argument( - "-d", - "--debug", - action="store_true", - help="Enable debugging mode in the k8s API.", - ) - - parser.add_argument( - "-q", - "--quiet", - action="store_true", - help="Suppress printing text on standard output.", - ) - - parser.add_argument( - "-V", - "--version", - action="version", - version=f"{__title__} {__version__}", - help="Display version information and exit.", - ) - - parser.add_argument( - "-h", "--help", action="help", help="Display this help text and exit." - ) - - args = parser.parse_args(argv) - - return args - - -@dataclasses.dataclass -class ContainerExtra: - "Data class, to storage extra informations about container." - - running: bool - image: str - identifier: str - - -@dataclasses.dataclass -class ContainerVersions: - "Data class, to storage software versions from container." - - python: list - java: list - - -@dataclasses.dataclass -class ContainerInfo: - "Data class, to storage multiple informations about container." - - namespace: str - pod: str - container: str - extra: ContainerExtra - versions: ContainerVersions = None - - -def is_container_running( - status: kubernetes.client.models.v1_container_status.V1ContainerStatus, -) -> bool: - """Function to determine if k8s cluster container is in running state. - - Args: - status: Single item from container_statuses list, that represents container status. - - Returns: - If container is in running state. - """ - - if status.state.terminated: - return False - - if status.state.waiting: - return False - - if not status.state.running: - return False - - return True - - -def list_all_containers( - api: kubernetes.client.api.core_v1_api.CoreV1Api, - field_selector: str, - namespace: Union[None, str], - check_istio_sidecars: bool, - istio_sidecar_name: str -) -> Iterable[ContainerInfo]: - """Get list of all containers names. - - Args: - api: Client of the k8s cluster API. - field_selector: Kubernetes field selector, to filter out containers objects. - namespace: Namespace to limit reading pods from - check_istio_sidecars: Flag to enable/disable istio sidecars check. - Default to False - istio_sidecar_name: If checking istio sidecars is disabled the name to filter - containers out - - Yields: - Objects for all containers in k8s cluster. - """ - - if namespace: - pods = api.list_namespaced_pod(namespace, field_selector=field_selector).items - else: - pods = api.list_pod_for_all_namespaces(field_selector=field_selector).items - - containers_statuses = ( - (pod.metadata.namespace, pod.metadata.name, pod.status.container_statuses) - for pod in pods - if pod.status.container_statuses - ) - - containers_status = ( - itertools.product([namespace], [pod], statuses) - for namespace, pod, statuses in containers_statuses - ) - - containers_chained = itertools.chain.from_iterable(containers_status) - - containers_fields = ( - ( - namespace, - pod, - status.name, - is_container_running(status), - status.image, - status.container_id, - ) - for namespace, pod, status in containers_chained - ) - - container_items = ( - ContainerInfo( - namespace, pod, container, ContainerExtra(running, image, identifier) - ) - for namespace, pod, container, running, image, identifier in containers_fields - ) - - if not check_istio_sidecars: - container_items = filter(lambda container: container.container != istio_sidecar_name, container_items) - - yield from container_items - - -def sync_post_namespaced_pod_exec( - api: kubernetes.client.api.core_v1_api.CoreV1Api, - container: ContainerInfo, - command: Union[List[str], str], -) -> dict: - """Function to execute command on selected container. - - Args: - api: Client of the k8s cluster API. - container: Object, that represents container in k8s cluster. - command: Command to execute as a list of arguments or single string. - - Returns: - Dictionary that store informations about command execution. - * stdout - Standard output captured from execution. - * stderr - Standard error captured from execution. - * error - Error object that was received from kubernetes API. - * code - Exit code returned by executed process - or -1 if container is not running - or -2 if other failure occurred. - """ - - try: - client = kubernetes.stream.stream( - api.connect_post_namespaced_pod_exec, - namespace=container.namespace, - name=container.pod, - container=container.container, - command=command, - stderr=True, - stdin=False, - stdout=True, - tty=False, - _request_timeout=1.0, - _preload_content=False, - ) - except ( - kubernetes.client.rest.ApiException, - kubernetes.client.exceptions.ApiException, - ): - - if container.extra.running: - raise - - return { - "stdout": "", - "stderr": "", - "error": {}, - "code": -1, - } - - client.run_forever(timeout=5) - - stdout = client.read_stdout() - stderr = client.read_stderr() - error = yaml.safe_load( - client.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL) - ) - - # TODO: Is there really no better way, to check - # execution exit code in python k8s API client? - code = -2 - try: - code = ( - 0 - if error["status"] == "Success" - else -2 - if error["reason"] != "NonZeroExitCode" - else int(error["details"]["causes"][0]["message"]) - ) - except: - pass - - return { - "stdout": stdout, - "stderr": stderr, - "error": error, - "code": code, - } - - -def generate_python_binaries() -> List[str]: - """Function to generate list of names and paths for CPython binaries. - - Returns: - List of names and paths, to CPython binaries. - """ - - dirnames = ["", "/usr/bin/", "/usr/local/bin/"] - - majors_minors = [ - f"{major}.{minor}" for major, minor in itertools.product("23", string.digits) - ] - - suffixes = ["", "2", "3"] + majors_minors - - basenames = [f"python{suffix}" for suffix in suffixes] - - binaries = [f"{dir}{base}" for dir, base in itertools.product(dirnames, basenames)] - - return binaries - - -def generate_java_binaries() -> List[str]: - """Function to generate list of names and paths for OpenJDK binaries. - - Returns: - List of names and paths, to OpenJDK binaries. - """ - - binaries = [ - "java", - "/usr/bin/java", - "/usr/local/bin/java", - "/etc/alternatives/java", - "/usr/java/openjdk-14/bin/java", - ] - - return binaries - - -def determine_versions_abstraction( - api: kubernetes.client.api.core_v1_api.CoreV1Api, - container: ContainerInfo, - binaries: List[str], - extractor: Pattern, -) -> List[str]: - """Function to determine list of software versions, that are installed in - given container. - - Args: - api: Client of the k8s cluster API. - container: Object, that represents container in k8s cluster. - binaries: List of names and paths to the abstract software binaries. - extractor: Pattern to extract the version string from the output of the binary execution. - - Returns: - List of installed software versions. - """ - - commands = ([binary, "--version"] for binary in binaries) - commands_old = ([binary, "-version"] for binary in binaries) - commands_all = itertools.chain(commands, commands_old) - - # TODO: This list comprehension should be parallelized - results = ( - sync_post_namespaced_pod_exec(api, container, command) - for command in commands_all - ) - - successes = ( - f"{result['stdout']}{result['stderr']}" - for result in results - if result["code"] == 0 - ) - - extractions = (extractor.search(success) for success in successes) - - versions = sorted( - set(extraction.group(1) for extraction in extractions if extraction) - ) - - return versions - - -def determine_versions_of_python( - api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo -) -> List[str]: - """Function to determine list of CPython versions, - that are installed in given container. - - Args: - api: Client of the k8s cluster API. - container: Object, that represents container in k8s cluster. - - Returns: - List of installed CPython versions. - """ - - extractor = re.compile("Python ([0-9.]+)") - - binaries = generate_python_binaries() - - versions = determine_versions_abstraction(api, container, binaries, extractor) - - return versions - - -def determine_versions_of_java( - api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo -) -> List[str]: - """Function to determine list of OpenJDK versions, - that are installed in given container. - - Args: - api: Client of the k8s cluster API. - container: Object, that represents container in k8s cluster. - - Returns: - List of installed OpenJDK versions. - """ - - extractor = re.compile('openjdk [version" ]*([0-9._]+)') - - binaries = generate_java_binaries() - - versions = determine_versions_abstraction(api, container, binaries, extractor) - - return versions - - -def gather_containers_informations( - api: kubernetes.client.api.core_v1_api.CoreV1Api, - field_selector: str, - ignore_empty: bool, - namespace: Union[None, str], - check_istio_sidecars: bool, - istio_sidecar_name: str -) -> List[ContainerInfo]: - """Get list of all containers names. - - Args: - api: Client of the k8s cluster API. - field_selector: Kubernetes field selector, to filter out containers objects. - ignore_empty: Determines, if containers with empty versions should be ignored. - namespace: Namespace to limit reading pods from - check_istio_sidecars: Flag to enable/disable istio sidecars check. - Default to False - istio_sidecar_name: If checking istio sidecars is disabled the name to filter - containers out - - Returns: - List of initialized objects for containers in k8s cluster. - """ - - containers = list(list_all_containers(api, field_selector, namespace, - check_istio_sidecars, istio_sidecar_name)) - - # TODO: This loop should be parallelized - for container in containers: - python_versions = determine_versions_of_python(api, container) - java_versions = determine_versions_of_java(api, container) - container.versions = ContainerVersions(python_versions, java_versions) - - if ignore_empty: - containers = [c for c in containers if c.versions.python or c.versions.java] - - return containers - - -def generate_output_tabulate(containers: Iterable[ContainerInfo]) -> str: - """Function for generate output string in tabulate format. - - Args: - containers: List of items, that represents containers in k8s cluster. - - Returns: - Output string formatted by tabulate module. - """ - - headers = [ - "Namespace", - "Pod", - "Container", - "Running", - "CPython", - "OpenJDK", - ] - - rows = [ - [ - container.namespace, - container.pod, - container.container, - container.extra.running, - " ".join(container.versions.python), - " ".join(container.versions.java), - ] - for container in containers - ] - - output = tabulate.tabulate(rows, headers=headers) - - return output - - -def generate_output_pprint(containers: Iterable[ContainerInfo]) -> str: - """Function for generate output string in pprint format. - - Args: - containers: List of items, that represents containers in k8s cluster. - - Returns: - Output string formatted by pprint module. - """ - - output = pprint.pformat(containers) - - return output - - -def generate_output_json(containers: Iterable[ContainerInfo]) -> str: - """Function for generate output string in JSON format. - - Args: - containers: List of items, that represents containers in k8s cluster. - - Returns: - Output string formatted by json module. - """ - - data = [ - { - "namespace": container.namespace, - "pod": container.pod, - "container": container.container, - "extra": { - "running": container.extra.running, - "image": container.extra.image, - "identifier": container.extra.identifier, - }, - "versions": { - "python": container.versions.python, - "java": container.versions.java, - }, - } - for container in containers - ] - - output = json.dumps(data, indent=4) - - return output - - -def generate_and_handle_output( - containers: List[ContainerInfo], - output_format: str, - output_file: pathlib.Path, - quiet: bool, -) -> None: - """Generate and handle the output of the containers software versions. - - Args: - containers: List of items, that represents containers in k8s cluster. - output_format: String that will determine output format (tabulate, pprint, json). - output_file: Path to file, where output will be save. - quiet: Determines if output should be printed, to stdout. - """ - - output_generators = { - "tabulate": generate_output_tabulate, - "pprint": generate_output_pprint, - "json": generate_output_json, - } - - output = output_generators[output_format](containers) - - if output_file: - output_file.write_text(output) - - if not quiet: - print(output) - - -def verify_versions_acceptability( - containers: List[ContainerInfo], acceptable: pathlib.Path, quiet: bool -) -> bool: - """Function for verification of software versions installed in containers. - - Args: - containers: List of items, that represents containers in k8s cluster. - acceptable: Path to the YAML file, with the software verification parameters. - quiet: Determines if output should be printed, to stdout. - - Returns: - 0 if the verification was successful or 1 otherwise. - """ - - if not acceptable: - return 0 - - if not acceptable.is_file(): - raise FileNotFoundError( - "File with configuration for acceptable does not exists!" - ) - - schema = { - "python": {"type": "list", "schema": {"type": "string"}}, - "java": {"type": "list", "schema": {"type": "string"}}, - } - - validator = cerberus.Validator(schema) - - with open(acceptable) as stream: - data = yaml.safe_load(stream) - - if not validator.validate(data): - raise cerberus.SchemaError( - "Schema of file with configuration for acceptable is not valid." - ) - - python_acceptable = data.get("python", []) - java_acceptable = data.get("java", []) - - python_not_acceptable = [ - (container, "python", version) - for container in containers - for version in container.versions.python - if version not in python_acceptable - ] - - java_not_acceptable = [ - (container, "java", version) - for container in containers - for version in container.versions.java - if version not in java_acceptable - ] - - if not python_not_acceptable and not java_not_acceptable: - return 0 - - if quiet: - return 1 - - print("List of not acceptable versions") - pprint.pprint(python_not_acceptable) - pprint.pprint(java_not_acceptable) - - return 1 - - -def main(argv: Optional[List[str]] = None) -> str: - """Main entrypoint of the module for verifying versions of CPython and - OpenJDK installed in k8s cluster containers. - - Args: - argv: List of command line arguments. - """ - - args = parse_argv(argv) - - kubernetes.config.load_kube_config(args.config_file) - - api = kubernetes.client.CoreV1Api() - api.api_client.configuration.debug = args.debug - - containers = gather_containers_informations( - api, args.field_selector, args.ignore_empty, args.namespace, - args.check_istio_sidecar, args.istio_sidecar_name - ) - - generate_and_handle_output( - containers, args.output_format, args.output_file, args.quiet - ) - - code = verify_versions_acceptability(containers, args.acceptable, args.quiet) - - return code - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/test/security/check_versions/tests/test_main.py b/test/security/check_versions/tests/test_main.py index 0dff0b230..37ad45ee3 100644 --- a/test/security/check_versions/tests/test_main.py +++ b/test/security/check_versions/tests/test_main.py @@ -7,9 +7,7 @@ import yaml def exec_main(pod_name_trimmer, acceptable_data): - with tempfile.NamedTemporaryFile() as output_temp, tempfile.NamedTemporaryFile() as acceptable_temp: - with open(acceptable_temp.name, "w") as stream: yaml.safe_dump(acceptable_data, stream) @@ -61,7 +59,6 @@ def exec_main(pod_name_trimmer, acceptable_data): def test_main(pod_name_trimmer): - acceptable_data = { "python": ["2.7.5", "3.6.6", "3.8.4"], "java": ["11.0.5", "11.0.8"], @@ -73,7 +70,6 @@ def test_main(pod_name_trimmer): def test_main_neg(pod_name_trimmer): - acceptable_data = { "python": ["3.6.6", "3.8.4"], "java": ["11.0.5", "11.0.8"], diff --git a/test/security/check_versions/tests/test_verify_versions_acceptability.py b/test/security/check_versions/tests/test_verify_versions_acceptability.py index 5e2f0d2c8..1cb931679 100644 --- a/test/security/check_versions/tests/test_verify_versions_acceptability.py +++ b/test/security/check_versions/tests/test_verify_versions_acceptability.py @@ -7,7 +7,6 @@ import pathlib def exec_verify_versions_acceptability(containers): - config = { "python": ["1.1.1", "2.2.2"], "java": ["3.3.3"], @@ -23,7 +22,6 @@ def exec_verify_versions_acceptability(containers): def test_verify_versions_acceptability(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions([], [])), kbvi.ContainerInfo( @@ -37,7 +35,6 @@ def test_verify_versions_acceptability(): def test_verify_versions_acceptability_neg_1(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions(["3.3.3"], [])) ] @@ -48,7 +45,6 @@ def test_verify_versions_acceptability_neg_1(): def test_verify_versions_acceptability_neg_2(): - containers = [ kbvi.ContainerInfo("a", "b", "c", None, kbvi.ContainerVersions([], ["1.1.1"])) ] diff --git a/test/security/check_versions/tox.ini b/test/security/check_versions/tox.ini index 703ee280a..d2a007160 100644 --- a/test/security/check_versions/tox.ini +++ b/test/security/check_versions/tox.ini @@ -1,16 +1,18 @@ [tox] -envlist = black, pylint +envlist = black, pylint, pytest skipsdist = true [testenv] basepython = python3.8 -deps = -r{toxinidir}/env/requirements-dev.txt +deps = -r{toxinidir}/requirements.txt [testenv:black] -commands = black {toxinidir}/src tests +commands = black {toxinidir}/versions tests +deps = black [testenv:pylint] -commands = pylint -d C0330,W0511 {toxinidir}/src +commands = pylint -d C0330,W0511 {toxinidir}/versions +deps= pylint [testenv:pytest] setenv = PYTHONPATH = {toxinidir}/src diff --git a/test/security/check_versions/versions/__init__.py b/test/security/check_versions/versions/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/test/security/check_versions/versions/k8s_bin_versions_inspector.py b/test/security/check_versions/versions/k8s_bin_versions_inspector.py new file mode 100644 index 000000000..bd3041d63 --- /dev/null +++ b/test/security/check_versions/versions/k8s_bin_versions_inspector.py @@ -0,0 +1,769 @@ +#!/usr/bin/env python3 + +# COPYRIGHT NOTICE STARTS HERE +# +# Copyright 2020 Samsung Electronics Co., Ltd. +# Copyright 2023 Deutsche Telekom AG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# COPYRIGHT NOTICE ENDS HERE + +""" +k8s_bin_versions_inspector is a module for verifying versions of CPython and +OpenJDK binaries installed in the kubernetes cluster containers. +""" + +__title__ = "k8s_bin_versions_inspector" +__summary__ = ( + "Module for verifying versions of CPython and OpenJDK binaries installed" + " in the kubernetes cluster containers." +) +__version__ = "0.1.0" +__author__ = "kkkk.k@samsung.com" +__license__ = "Apache-2.0" +__copyright__ = "Copyright 2020 Samsung Electronics Co., Ltd." + +from typing import Iterable, List, Optional, Pattern, Union + +import argparse +import dataclasses +import itertools +import json +import logging +import pathlib +import pprint +import re +import string +import sys +from typing import Iterable, List, Optional, Pattern, Union +import tabulate +import yaml + +import kubernetes + +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + +# Logger +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-inspector") +LOGGER.setLevel("INFO") + + +def parse_argv(argv: Optional[List[str]] = None) -> argparse.Namespace: + """Function for parsing command line arguments. + + Args: + argv: Unparsed list of command line arguments. + + Returns: + Namespace with values from parsed arguments. + """ + + epilog = ( + f"Author: {__author__}\n" + f"License: {__license__}\n" + f"Copyright: {__copyright__}\n" + ) + + parser = argparse.ArgumentParser( + formatter_class=argparse.RawTextHelpFormatter, + prog=__title__, + description=__summary__, + epilog=epilog, + add_help=False, + ) + + parser.add_argument("-c", "--config-file", help="Name of the kube-config file.") + + parser.add_argument( + "-s", + "--field-selector", + default="", + help="Kubernetes field selector, to filter out containers objects.", + ) + + parser.add_argument( + "-o", + "--output-file", + type=pathlib.Path, + help="Path to file, where output will be saved.", + ) + + parser.add_argument( + "-f", + "--output-format", + choices=("tabulate", "pprint", "json"), + default="tabulate", + help="Format of the output file (tabulate, pprint, json).", + ) + + parser.add_argument( + "-i", + "--ignore-empty", + action="store_true", + help="Ignore containers without any versions.", + ) + + parser.add_argument( + "-a", + "--acceptable", + type=pathlib.Path, + help="Path to YAML file, with list of acceptable software versions.", + ) + + parser.add_argument( + "-n", + "--namespace", + help="Namespace to use to list pods." + "If empty pods are going to be listed from all namespaces", + ) + + parser.add_argument( + "--check-istio-sidecar", + action="store_true", + help="Add if you want to check istio sidecars also", + ) + + parser.add_argument( + "--istio-sidecar-name", + default="istio-proxy", + help="Name of istio sidecar to filter out", + ) + + parser.add_argument( + "-d", + "--debug", + action="store_true", + help="Enable debugging mode in the k8s API.", + ) + + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Suppress printing text on standard output.", + ) + + parser.add_argument( + "-w", + "--waiver", + type=pathlib.Path, + help="Path of the waiver xfail file.", + ) + + parser.add_argument( + "-V", + "--version", + action="version", + version=f"{__title__} {__version__}", + help="Display version information and exit.", + ) + + parser.add_argument( + "-h", "--help", action="help", help="Display this help text and exit." + ) + + args = parser.parse_args(argv) + + return args + + +@dataclasses.dataclass +class ContainerExtra: + "Data class, to storage extra informations about container." + + running: bool + image: str + identifier: str + + +@dataclasses.dataclass +class ContainerVersions: + "Data class, to storage software versions from container." + + python: list + java: list + + +@dataclasses.dataclass +class ContainerInfo: + "Data class, to storage multiple informations about container." + + namespace: str + pod: str + container: str + extra: ContainerExtra + versions: ContainerVersions = None + + +def is_container_running( + status: kubernetes.client.models.v1_container_status.V1ContainerStatus, +) -> bool: + """Function to determine if k8s cluster container is in running state. + + Args: + status: Single item from container_statuses list, that represents container status. + + Returns: + If container is in running state. + """ + + if status.state.terminated: + return False + + if status.state.waiting: + return False + + if not status.state.running: + return False + + return True + + +def list_all_containers( + api: kubernetes.client.api.core_v1_api.CoreV1Api, + field_selector: str, + namespace: Union[None, str], + check_istio_sidecars: bool, + istio_sidecar_name: str, +) -> Iterable[ContainerInfo]: + """Get list of all containers names. + + Args: + api: Client of the k8s cluster API. + field_selector: Kubernetes field selector, to filter out containers objects. + namespace: Namespace to limit reading pods from + check_istio_sidecars: Flag to enable/disable istio sidecars check. + Default to False + istio_sidecar_name: If checking istio sidecars is disabled the name to filter + containers out + + Yields: + Objects for all containers in k8s cluster. + """ + + if namespace: + pods = api.list_namespaced_pod(namespace, field_selector=field_selector).items + else: + pods = api.list_pod_for_all_namespaces(field_selector=field_selector).items + + # Filtering to avoid testing integration or replica pods + pods = [ + pod + for pod in pods + if "replica" not in pod.metadata.name and "integration" not in pod.metadata.name + ] + + containers_statuses = ( + (pod.metadata.namespace, pod.metadata.name, pod.status.container_statuses) + for pod in pods + if pod.status.container_statuses + ) + + containers_status = ( + itertools.product([namespace], [pod], statuses) + for namespace, pod, statuses in containers_statuses + ) + + containers_chained = itertools.chain.from_iterable(containers_status) + + containers_fields = ( + ( + namespace, + pod, + status.name, + is_container_running(status), + status.image, + status.container_id, + ) + for namespace, pod, status in containers_chained + ) + + container_items = ( + ContainerInfo( + namespace, pod, container, ContainerExtra(running, image, identifier) + ) + for namespace, pod, container, running, image, identifier in containers_fields + ) + + if not check_istio_sidecars: + container_items = filter( + lambda container: container.container != istio_sidecar_name, container_items + ) + + yield from container_items + + +def sync_post_namespaced_pod_exec( + api: kubernetes.client.api.core_v1_api.CoreV1Api, + container: ContainerInfo, + command: Union[List[str], str], +) -> dict: + """Function to execute command on selected container. + + Args: + api: Client of the k8s cluster API. + container: Object, that represents container in k8s cluster. + command: Command to execute as a list of arguments or single string. + + Returns: + Dictionary that store informations about command execution. + * stdout - Standard output captured from execution. + * stderr - Standard error captured from execution. + * error - Error object that was received from kubernetes API. + * code - Exit code returned by executed process + or -1 if container is not running + or -2 if other failure occurred. + """ + + stdout = "" + stderr = "" + error = {} + code = -1 + LOGGER.debug("sync_post_namespaced_pod_exec container= %s", container.pod) + try: + client_stream = kubernetes.stream.stream( + api.connect_post_namespaced_pod_exec, + namespace=container.namespace, + name=container.pod, + container=container.container, + command=command, + stderr=True, + stdin=False, + stdout=True, + tty=False, + _request_timeout=1.0, + _preload_content=False, + ) + client_stream.run_forever(timeout=5) + stdout = client_stream.read_stdout() + stderr = client_stream.read_stderr() + error = yaml.safe_load( + client_stream.read_channel(kubernetes.stream.ws_client.ERROR_CHANNEL) + ) + + code = ( + 0 + if error["status"] == "Success" + else -2 + if error["reason"] != "NonZeroExitCode" + else int(error["details"]["causes"][0]["message"]) + ) + except ( + kubernetes.client.rest.ApiException, + kubernetes.client.exceptions.ApiException, + ): + LOGGER.debug("Discard unexpected k8s client Error..") + except TypeError: + LOGGER.debug("Type Error, no error status") + pass + + return { + "stdout": stdout, + "stderr": stderr, + "error": error, + "code": code, + } + + +def generate_python_binaries() -> List[str]: + """Function to generate list of names and paths for CPython binaries. + + Returns: + List of names and paths, to CPython binaries. + """ + + dirnames = ["", "/usr/bin/", "/usr/local/bin/"] + + majors_minors = [ + f"{major}.{minor}" for major, minor in itertools.product("23", string.digits) + ] + + suffixes = ["", "2", "3"] + majors_minors + + basenames = [f"python{suffix}" for suffix in suffixes] + + binaries = [f"{dir}{base}" for dir, base in itertools.product(dirnames, basenames)] + + return binaries + + +def generate_java_binaries() -> List[str]: + """Function to generate list of names and paths for OpenJDK binaries. + + Returns: + List of names and paths, to OpenJDK binaries. + """ + + binaries = [ + "java", + "/usr/bin/java", + "/usr/local/bin/java", + "/etc/alternatives/java", + "/usr/java/openjdk-14/bin/java", + ] + + return binaries + + +def determine_versions_abstraction( + api: kubernetes.client.api.core_v1_api.CoreV1Api, + container: ContainerInfo, + binaries: List[str], + extractor: Pattern, +) -> List[str]: + """Function to determine list of software versions, that are installed in + given container. + + Args: + api: Client of the k8s cluster API. + container: Object, that represents container in k8s cluster. + binaries: List of names and paths to the abstract software binaries. + extractor: Pattern to extract the version string from the output of the binary execution. + + Returns: + List of installed software versions. + """ + + commands = ([binary, "--version"] for binary in binaries) + commands_old = ([binary, "-version"] for binary in binaries) + commands_all = itertools.chain(commands, commands_old) + + # TODO: This list comprehension should be parallelized + results = ( + sync_post_namespaced_pod_exec(api, container, command) + for command in commands_all + ) + + successes = ( + f"{result['stdout']}{result['stderr']}" + for result in results + if result["code"] == 0 + ) + + extractions = (extractor.search(success) for success in successes) + + versions = sorted( + set(extraction.group(1) for extraction in extractions if extraction) + ) + + return versions + + +def determine_versions_of_python( + api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo +) -> List[str]: + """Function to determine list of CPython versions, + that are installed in given container. + + Args: + api: Client of the k8s cluster API. + container: Object, that represents container in k8s cluster. + + Returns: + List of installed CPython versions. + """ + + extractor = re.compile("Python ([0-9.]+)") + + binaries = generate_python_binaries() + + versions = determine_versions_abstraction(api, container, binaries, extractor) + + return versions + + +def determine_versions_of_java( + api: kubernetes.client.api.core_v1_api.CoreV1Api, container: ContainerInfo +) -> List[str]: + """Function to determine list of OpenJDK versions, + that are installed in given container. + + Args: + api: Client of the k8s cluster API. + container: Object, that represents container in k8s cluster. + + Returns: + List of installed OpenJDK versions. + """ + + extractor = re.compile('openjdk [version" ]*([0-9._]+)') + + binaries = generate_java_binaries() + + versions = determine_versions_abstraction(api, container, binaries, extractor) + + return versions + + +def gather_containers_informations( + api: kubernetes.client.api.core_v1_api.CoreV1Api, + field_selector: str, + ignore_empty: bool, + namespace: Union[None, str], + check_istio_sidecars: bool, + istio_sidecar_name: str, +) -> List[ContainerInfo]: + """Get list of all containers names. + + Args: + api: Client of the k8s cluster API. + field_selector: Kubernetes field selector, to filter out containers objects. + ignore_empty: Determines, if containers with empty versions should be ignored. + namespace: Namespace to limit reading pods from + check_istio_sidecars: Flag to enable/disable istio sidecars check. + Default to False + istio_sidecar_name: If checking istio sidecars is disabled the name to filter + containers out + + Returns: + List of initialized objects for containers in k8s cluster. + """ + + containers = list( + list_all_containers( + api, field_selector, namespace, check_istio_sidecars, istio_sidecar_name + ) + ) + LOGGER.info("List of containers: %s", containers) + + # TODO: This loop should be parallelized + for container in containers: + LOGGER.info("Container -----------------> %s", container) + python_versions = determine_versions_of_python(api, container) + java_versions = determine_versions_of_java(api, container) + container.versions = ContainerVersions(python_versions, java_versions) + LOGGER.info("Container versions: %s", container.versions) + + if ignore_empty: + containers = [c for c in containers if c.versions.python or c.versions.java] + + return containers + + +def generate_output_tabulate(containers: Iterable[ContainerInfo]) -> str: + """Function for generate output string in tabulate format. + + Args: + containers: List of items, that represents containers in k8s cluster. + + Returns: + Output string formatted by tabulate module. + """ + + headers = [ + "Namespace", + "Pod", + "Container", + "Running", + "CPython", + "OpenJDK", + ] + + rows = [ + [ + container.namespace, + container.pod, + container.container, + container.extra.running, + " ".join(container.versions.python), + " ".join(container.versions.java), + ] + for container in containers + ] + + output = tabulate.tabulate(rows, headers=headers) + + return output + + +def generate_output_pprint(containers: Iterable[ContainerInfo]) -> str: + """Function for generate output string in pprint format. + + Args: + containers: List of items, that represents containers in k8s cluster. + + Returns: + Output string formatted by pprint module. + """ + + output = pprint.pformat(containers) + + return output + + +def generate_output_json(containers: Iterable[ContainerInfo]) -> str: + """Function for generate output string in JSON format. + + Args: + containers: List of items, that represents containers in k8s cluster. + + Returns: + Output string formatted by json module. + """ + + data = [ + { + "namespace": container.namespace, + "pod": container.pod, + "container": container.container, + "extra": { + "running": container.extra.running, + "image": container.extra.image, + "identifier": container.extra.identifier, + }, + "versions": { + "python": container.versions.python, + "java": container.versions.java, + }, + } + for container in containers + ] + + output = json.dumps(data, indent=4) + + return output + + +def generate_and_handle_output( + containers: List[ContainerInfo], + output_format: str, + output_file: pathlib.Path, + quiet: bool, +) -> None: + """Generate and handle the output of the containers software versions. + + Args: + containers: List of items, that represents containers in k8s cluster. + output_format: String that will determine output format (tabulate, pprint, json). + output_file: Path to file, where output will be save. + quiet: Determines if output should be printed, to stdout. + """ + + output_generators = { + "tabulate": generate_output_tabulate, + "pprint": generate_output_pprint, + "json": generate_output_json, + } + LOGGER.debug("output_generators: %s", output_generators) + + output = output_generators[output_format](containers) + + if output_file: + try: + output_file.write_text(output) + except AttributeError: + LOGGER.error("Not possible to write_text") + + if not quiet: + LOGGER.info(output) + + +def verify_versions_acceptability( + containers: List[ContainerInfo], acceptable: pathlib.Path, quiet: bool +) -> bool: + """Function for verification of software versions installed in containers. + + Args: + containers: List of items, that represents containers in k8s cluster. + acceptable: Path to the YAML file, with the software verification parameters. + quiet: Determines if output should be printed, to stdout. + + Returns: + 0 if the verification was successful or 1 otherwise. + """ + + if not acceptable: + return 0 + + try: + acceptable.is_file() + except AttributeError: + LOGGER.error("No acceptable file found") + return -1 + + if not acceptable.is_file(): + raise FileNotFoundError( + "File with configuration for acceptable does not exists!" + ) + + with open(acceptable) as stream: + data = yaml.safe_load(stream) + + python_acceptable = data.get("python3", []) + java_acceptable = data.get("java11", []) + + python_not_acceptable = [ + (container, "python3", version) + for container in containers + for version in container.versions.python + if version not in python_acceptable + ] + + java_not_acceptable = [ + (container, "java11", version) + for container in containers + for version in container.versions.java + if version not in java_acceptable + ] + + if not python_not_acceptable and not java_not_acceptable: + return 0 + + if quiet: + return 1 + + LOGGER.error("List of not acceptable versions") + pprint.pprint(python_not_acceptable) + pprint.pprint(java_not_acceptable) + + return 1 + + +def main(argv: Optional[List[str]] = None) -> str: + """Main entrypoint of the module for verifying versions of CPython and + OpenJDK installed in k8s cluster containers. + + Args: + argv: List of command line arguments. + """ + + args = parse_argv(argv) + + kubernetes.config.load_kube_config(args.config_file) + + api = kubernetes.client.CoreV1Api() + api.api_client.configuration.debug = args.debug + + containers = gather_containers_informations( + api, + args.field_selector, + args.ignore_empty, + args.namespace, + args.check_istio_sidecar, + args.istio_sidecar_name, + ) + + generate_and_handle_output( + containers, args.output_format, args.output_file, args.quiet + ) + + code = verify_versions_acceptability(containers, args.acceptable, args.quiet) + + return code + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py b/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py new file mode 100644 index 000000000..87516cb60 --- /dev/null +++ b/test/security/check_versions/versions/k8s_bin_versions_inspector_test_case.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 + +# COPYRIGHT NOTICE STARTS HERE +# +# Copyright 2020 Samsung Electronics Co., Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# COPYRIGHT NOTICE ENDS HERE + +import logging +import pathlib +import time +import os +import wget +from kubernetes import client, config +from xtesting.core import testcase # pylint: disable=import-error + +import versions.reporting as Reporting +from versions.k8s_bin_versions_inspector import ( + gather_containers_informations, + generate_and_handle_output, + verify_versions_acceptability, +) + +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + +# Logger +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-inspector") +LOGGER.setLevel("INFO") + + +class Inspector(testcase.TestCase): + """Inspector CLass.""" + + def __init__(self, **kwargs): + """Init the testcase.""" + if "case_name" not in kwargs: + kwargs["case_name"] = "check_versions" + super().__init__(**kwargs) + + version = os.getenv("ONAP_VERSION", "master") + base_url = "https://git.onap.org/integration/seccom/plain" + + self.namespace = "onap" + # if no Recommended file found, download it + if pathlib.Path(RECOMMENDED_VERSIONS_FILE).is_file(): + self.acceptable = pathlib.Path(RECOMMENDED_VERSIONS_FILE) + else: + self.acceptable = wget.download( + base_url + "/recommended_versions.yaml?h=" + version, + out=RECOMMENDED_VERSIONS_FILE, + ) + self.output_file = "/tmp/versions.json" + # if no waiver file found, download it + if pathlib.Path(WAIVER_LIST_FILE).is_file(): + self.waiver = pathlib.Path(WAIVER_LIST_FILE) + else: + self.waiver = wget.download( + base_url + "/waivers/versions/versions_xfail.txt?h=" + version, + out=WAIVER_LIST_FILE, + ) + self.result = 0 + self.start_time = None + self.stop_time = None + + def run(self): + """Execute the version Inspector.""" + self.start_time = time.time() + config.load_kube_config() + api = client.CoreV1Api() + + field_selector = "metadata.namespace==onap" + + containers = gather_containers_informations(api, field_selector, True) + LOGGER.info("gather_containers_informations") + LOGGER.info(containers) + LOGGER.info("---------------------------------") + + generate_and_handle_output( + containers, "json", pathlib.Path(self.output_file), True + ) + LOGGER.info("generate_and_handle_output in %s", self.output_file) + LOGGER.info("---------------------------------") + + code = verify_versions_acceptability(containers, self.acceptable, True) + LOGGER.info("verify_versions_acceptability") + LOGGER.info(code) + LOGGER.info("---------------------------------") + + # Generate reporting + test = Reporting.OnapVersionsReporting(result_file=self.output_file) + LOGGER.info("Prepare reporting") + self.result = test.generate_reporting(self.output_file) + LOGGER.info("Reporting generated") + + self.stop_time = time.time() + if self.result >= 90: + return testcase.TestCase.EX_OK + return testcase.TestCase.EX_TESTCASE_FAILED + + def set_namespace(self, namespace): + """Set namespace.""" + self.namespace = namespace diff --git a/test/security/check_versions/versions/reporting.py b/test/security/check_versions/versions/reporting.py new file mode 100644 index 000000000..43ef26db8 --- /dev/null +++ b/test/security/check_versions/versions/reporting.py @@ -0,0 +1,265 @@ +#!/usr/bin/env python3 + +# Copyright 2020 Orange, Ltd. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +""" +Generate result page +""" +import logging +import pathlib +import json +from dataclasses import dataclass +import os +import statistics +import wget +import yaml + +from packaging.version import Version + +from jinja2 import ( # pylint: disable=import-error + Environment, + select_autoescape, + PackageLoader, +) + +# Logger +LOG_LEVEL = "INFO" +logging.basicConfig() +LOGGER = logging.getLogger("onap-versions-status-reporting") +LOGGER.setLevel(LOG_LEVEL) + +REPORTING_FILE = "/var/lib/xtesting/results/versions_reporting.html" +# REPORTING_FILE = "/tmp/versions_reporting.html" +RESULT_FILE = "/tmp/versions.json" +RECOMMENDED_VERSIONS_FILE = "/tmp/recommended_versions.yaml" +WAIVER_LIST_FILE = "/tmp/versions_xfail.txt" + + +@dataclass +class TestResult: + """Test results retrieved from xtesting.""" + + pod_name: str + container: str + image: str + python_version: str + python_status: int + java_version: str + java_status: int + + +@dataclass +class SerieResult: + """Serie of tests.""" + + serie_id: str + success_rate: int = 0 + min: int = 0 + max: int = 0 + mean: float = 0.0 + median: float = 0.0 + nb_occurences: int = 0 + + +class OnapVersionsReporting: + """Build html summary page.""" + + def __init__(self, result_file) -> None: + """Initialization of the report.""" + version = os.getenv("ONAP_VERSION", "master") + base_url = "https://git.onap.org/integration/seccom/plain" + if pathlib.Path(WAIVER_LIST_FILE).is_file(): + self._waiver_file = pathlib.Path(WAIVER_LIST_FILE) + else: + self._waiver_file = wget.download( + base_url + "/waivers/versions/versions_xfail.txt?h=" + version, + out=WAIVER_LIST_FILE, + ) + if pathlib.Path(RECOMMENDED_VERSIONS_FILE).is_file(): + self._recommended_versions_file = pathlib.Path(RECOMMENDED_VERSIONS_FILE) + else: + self._recommended_versions_file = wget.download( + base_url + "/recommended_versions.yaml?h=" + version, + out=RECOMMENDED_VERSIONS_FILE, + ) + + def get_versions_scan_results(self, result_file, waiver_list): + """Get all the versions from the scan.""" + testresult = [] + # Get the recommended version list for java and python + min_java_version = self.get_recommended_version( + RECOMMENDED_VERSIONS_FILE, "java11" + ) + min_python_version = self.get_recommended_version( + RECOMMENDED_VERSIONS_FILE, "python3" + ) + + LOGGER.info("Min Java recommended version: %s", min_java_version) + LOGGER.info("Min Python recommended version: %s", min_python_version) + + with open(result_file) as json_file: + data = json.load(json_file) + LOGGER.info("Number of pods: %s", len(data)) + for component in data: + if component["container"] not in waiver_list: + testresult.append( + TestResult( + pod_name=component["pod"], + container=component["container"], + image=component["extra"]["image"], + python_version=component["versions"]["python"], + java_version=component["versions"]["java"], + python_status=self.get_version_status( + component["versions"]["python"], min_python_version[0] + ), + java_status=self.get_version_status( + component["versions"]["java"], min_java_version[0] + ), + ) + ) + LOGGER.info("Nb of pods (after waiver filtering) %s", len(testresult)) + return testresult + + @staticmethod + def get_version_status(versions, min_version): + """Based on the min version set the status of the component version.""" + # status_code + # 0: only recommended version found + # 1: recommended version found but not alone + # 2: recommended version not found but not far + # 3: recommended version not found but not far but not alone + # 4: recommended version not found + # we assume that versions are given accordign to usual java way + # X.Y.Z + LOGGER.debug("Version = %s", versions) + LOGGER.debug("Min Version = %s", min_version) + nb_versions_found = len(versions) + status_code = -1 + LOGGER.debug("Nb versions found :%s", nb_versions_found) + # if no version found retrieved -1 + if nb_versions_found > 0: + for version in versions: + clean_version = Version(version.replace("_", ".")) + min_version_ok = str(min_version) + + if clean_version >= Version(min_version_ok): + if nb_versions_found < 2: + status_code = 0 + else: + status_code = 2 + elif clean_version.major >= Version(min_version_ok).major: + if nb_versions_found < 2: + status_code = 1 + else: + status_code = 3 + else: + status_code = 4 + LOGGER.debug("Version status code = %s", status_code) + return status_code + + @staticmethod + def get_recommended_version(recommended_versions_file, component): + """Retrieve data from the json file.""" + with open(recommended_versions_file) as stream: + data = yaml.safe_load(stream) + try: + recommended_version = data[component]["recommended_versions"] + except KeyError: + recommended_version = None + return recommended_version + + @staticmethod + def get_waiver_list(waiver_file_path): + """Get the waiver list.""" + pods_to_be_excluded = [] + with open(waiver_file_path) as waiver_list: + for line in waiver_list: + line = line.strip("\n") + line = line.strip("\t") + if not line.startswith("#"): + pods_to_be_excluded.append(line) + return pods_to_be_excluded + + @staticmethod + def get_score(component_type, scan_res): + # Look at the java and python results + # 0 = recommended version + # 1 = acceptable version + nb_good_versions = 0 + nb_results = 0 + + for res in scan_res: + if component_type == "java": + if res.java_status >= 0: + nb_results += 1 + if res.java_status < 2: + nb_good_versions += 1 + elif component_type == "python": + if res.python_status >= 0: + nb_results += 1 + if res.python_status < 2: + nb_good_versions += 1 + try: + return round(nb_good_versions * 100 / nb_results, 1) + except ZeroDivisionError: + LOGGER.error("Impossible to calculate the success rate") + return 0 + + def generate_reporting(self, result_file): + """Generate HTML reporting page.""" + LOGGER.info("Generate versions HTML report.") + + # Get the waiver list + waiver_list = self.get_waiver_list(self._waiver_file) + LOGGER.info("Waiver list: %s", waiver_list) + + # Get the Versions results + scan_res = self.get_versions_scan_results(result_file, waiver_list) + + LOGGER.info("scan_res: %s", scan_res) + + # Evaluate result + status_res = {"java": 0, "python": 0} + for component_type in "java", "python": + status_res[component_type] = self.get_score(component_type, scan_res) + + LOGGER.info("status_res: %s", status_res) + + # Calculate the average score + numbers = [status_res[key] for key in status_res] + mean_ = statistics.mean(numbers) + + # Create reporting page + jinja_env = Environment( + autoescape=select_autoescape(["html"]), + loader=PackageLoader("onap_check_versions"), + ) + page_info = { + "title": "ONAP Integration versions reporting", + "success_rate": status_res, + "mean": mean_, + } + jinja_env.get_template("versions.html.j2").stream( + info=page_info, data=scan_res + ).dump("{}".format(REPORTING_FILE)) + + return mean_ + + +if __name__ == "__main__": + test = OnapVersionsReporting( + RESULT_FILE, WAIVER_LIST_FILE, RECOMMENDED_VERSIONS_FILE + ) + test.generate_reporting(RESULT_FILE) diff --git a/test/security/check_versions/versions/templates/base.html.j2 b/test/security/check_versions/versions/templates/base.html.j2 new file mode 100644 index 000000000..025c0ad25 --- /dev/null +++ b/test/security/check_versions/versions/templates/base.html.j2 @@ -0,0 +1,232 @@ +{% macro color(failing, total) %} +{% if failing == 0 %} +is-success +{% else %} +{% if (failing / total) <= 0.1 %} +is-warning +{% else %} +is-danger +{% endif %} +{% endif %} +{% endmacro %} + +{% macro percentage(failing, total) %} +{{ ((total - failing) / total) | round }} +{% endmacro %} + +{% macro statistic(resource_name, failing, total) %} +{% set success = total - failing %} +
+
+

{{ resource_name | capitalize }}

+

{{ success }}/{{ total }}

+ {{ percentage(failing, total) }} +
+
+{% endmacro %} + +{% macro pods_table(pods) %} +
+ + + + + + + + + + + + {% for pod in pods %} + + + {% if pod.init_done %} + + {% else %} + + {% endif %} + + + {% if pod.init_done %} + + {% else %} + + {% endif %} + + {% endfor %} + +
NameReadyStatusReasonRestarts
{{ pod.k8s.metadata.name }}{{ pod.running_containers }}/{{ (pod.containers | length) }}Init:{{ pod.runned_init_containers }}/{{ (pod.init_containers | length) }}{{ pod.k8s.status.phase }}{{ pod.k8s.status.reason }}{{ pod.restart_count }}{{ pod.init_restart_count }}
+
+{% endmacro %} + +{% macro key_value_description_list(title, dict) %} +
{{ title | capitalize }}:
+
+ {% if dict %} + {% for key, value in dict.items() %} + {% if loop.first %} +
+ {% endif %} +
{{ key }}:
+
{{ value }}
+ {% if loop.last %} +
+ {% endif %} + {% endfor %} + {% endif %} +
+{% endmacro %} + +{% macro description(k8s) %} +
+

Description

+
+
+ {% if k8s.spec.type %} +
Type:
+
{{ k8s.spec.type }}
+ {% if (k8s.spec.type | lower) == "clusterip" %} +
Headless:
+
{% if (k8s.spec.cluster_ip | lower) == "none" %}Yes{% else %}No{% endif %}
+ {% endif %} + {% endif %} + {{ key_value_description_list('Labels', k8s.metadata.labels) | indent(width=6) }} + {{ key_value_description_list('Annotations', k8s.metadata.annotations) | indent(width=6) }} + {% if k8s.spec.selector %} + {% if k8s.spec.selector.match_labels %} + {{ key_value_description_list('Selector', k8s.spec.selector.match_labels) | indent(width=6) }} + {% else %} + {{ key_value_description_list('Selector', k8s.spec.selector) | indent(width=6) }} + {% endif %} + {% endif %} + {% if k8s.phase %} +
Status:
+
{{ k8s.phase }}
+ {% endif %} + {% if k8s.metadata.owner_references %} +
Controlled By:
+
{{ k8s.metadata.owner_references[0].kind }}/{{ k8s.metadata.owner_references[0].name }}
+ {% endif %} +
+
+
+{% endmacro %} + +{% macro pods_container(pods, parent, has_title=True) %} +
+ {% if has_title %} +

Pods

+ {% endif %} + {% if (pods | length) > 0 %} + {{ pods_table(pods) | indent(width=2) }} + {% else %} +
{{ parent }} has no pods!
+ {% endif %} +
+{% endmacro %} + +{% macro two_level_breadcrumb(title, name) %} +
+ +
+{% endmacro %} + +{% macro pod_parent_summary(title, name, failed_pods, pods) %} +{{ summary(title, name, [{'title': 'Pod', 'failing': failed_pods, 'total': (pods | length)}]) }} +{% endmacro %} + +{% macro number_ok(number, none_value, total=None) %} +{% if number %} +{% if total and number < total %} +{{ number }} +{% else %} +{{ number }} +{% endif %} +{% else %} +{{ none_value }} +{% endif %} +{% endmacro %} + +{% macro summary(title, name, statistics) %} +
+
+
+

+ {{ title | capitalize }} {{ name }} Summary +

+ +
+
+
+{% endmacro %} + + + + + + + Tests results - {% block title %}{% endblock %} + + + {% block more_head %}{% endblock %} + + + + + {% block content %}{% endblock %} + + + + + diff --git a/test/security/check_versions/versions/templates/versions.html.j2 b/test/security/check_versions/versions/templates/versions.html.j2 new file mode 100644 index 000000000..4860a72da --- /dev/null +++ b/test/security/check_versions/versions/templates/versions.html.j2 @@ -0,0 +1,85 @@ +{% extends "base.html.j2" %} +{% block title %}ONAPTEST Bench{% endblock %} + +{% block content %} +

{{ info.title }}

+ +
+ +
+
+

Results

+
+
+SECCOM recommended versions (global success rate: {{ info.mean }}): +
    +
  • Java: {{ info.success_rate.java }}%
  • +
  • Python: {{ info.success_rate.python }}%
  • +
+
+
+ +
+
+

Legend

+
+
+
SECCOM recommended version
+
Not the recommended version but at least the major version
+
Ambiguous versions but at least 1 is the SECCOM recommended version
+
Ambiguous versions but at least 1 is the major recommended version
+
Wrong Versions
+
+
+
+ +

JAVA versions

+ + + + + + + + + + {% for component in data %} + + + {% if component.java_version is defined and component.java_version|length > 0 %} + + + {% endif %} + + {% endfor %} + +
ComponentVersions
{{ component.container }}{{ component.java_version}}
+
+
+ +
+

Python versions

+ + + + + + + + + + {% for component in data %} + + {% if component.python_version is defined and component.python_version|length > 0 %} + + + {% endif %} + + {% endfor %} + +
ComponentVersions
{{ component.container }}{{ component.python_version}}
+
+ +{% endblock %} + + -- cgit 1.2.3-korg