diff --git a/src/ssh_audit/policy.py b/src/ssh_audit/policy.py index a93448c..4a8303a 100644 --- a/src/ssh_audit/policy.py +++ b/src/ssh_audit/policy.py @@ -513,18 +513,46 @@ macs = %s server_policy_descriptions = [] client_policy_descriptions = [] + latest_server_policies: Dict[str, Dict[str, Union[int, str]]] = {} + latest_client_policies: Dict[str, Dict[str, Union[int, str]]] = {} for policy_name, policy in BUILTIN_POLICIES.items(): - policy_description = "" - if verbose: - policy_description = "\"{:s}\": {:s}".format(policy_name, policy['changelog']) - else: + + # If not in verbose mode, only store the latest version of each policy. + if not verbose: policy_description = "\"{:s}\"".format(policy_name) - if policy['server_policy']: - server_policy_descriptions.append(policy_description) - else: - client_policy_descriptions.append(policy_description) + # Truncate the version off the policy name and obtain the version as an integer. (i.e.: "Platform X (version 3)" -> "Platform X", 3 + policy_name_no_version = "" + version = 0 + version_pos = policy_name.find(" (version ") + if version_pos != -1: + policy_name_no_version = policy_name[0:version_pos] + version = int(cast(str, policy['version'])) # Unit tests guarantee this to be parseable as an int. + d = latest_server_policies if policy['server_policy'] else latest_client_policies + if policy_name_no_version not in d: + d[policy_name_no_version] = {} + d[policy_name_no_version]['latest_version'] = version + d[policy_name_no_version]['description'] = policy_description + elif version > cast(int, d[policy_name_no_version]['latest_version']): # If an updated version of the policy was found, replace the old one. + d[policy_name_no_version]['latest_version'] = version + d[policy_name_no_version]['description'] = policy_description + else: # In verbose mode, return all policy versions. + policy_description = "\"{:s}\": {:s}".format(policy_name, policy['changelog']) + if policy['server_policy']: + server_policy_descriptions.append(policy_description) + else: + client_policy_descriptions.append(policy_description) + + # Now that we have references to the latest policies only, add their full descriptions to the lists for returning. + if not verbose: + for _, dd in latest_server_policies.items(): + server_policy_descriptions.append(cast(str, dd['description'])) + + for _, dd in latest_client_policies.items(): + client_policy_descriptions.append(cast(str, dd['description'])) + + # Sort the lists for better readability. server_policy_descriptions.sort() client_policy_descriptions.sort() return server_policy_descriptions, client_policy_descriptions diff --git a/src/ssh_audit/ssh_audit.py b/src/ssh_audit/ssh_audit.py index 96a59d6..4bd41a6 100755 --- a/src/ssh_audit/ssh_audit.py +++ b/src/ssh_audit/ssh_audit.py @@ -782,7 +782,7 @@ def list_policies(out: OutputBuffer, verbose: bool) -> None: out.fail("Error: no built-in policies found!") else: out.info("\nHint: Use -P and provide the full name of a policy to run a policy scan with.\n") - out.info("Hint: Use -L -v to also see the change log for each policy.\n") + out.info("Hint: Use -L -v to see the change log for each policy, as well as previous versions.\n") out.info("Note: the general OpenSSH policies apply to the official releases only. OS distributions may back-port changes that cause failures (for example, Debian 11 back-ported the strict KEX mode into their package of OpenSSH v8.4, whereas it was only officially added to OpenSSH v9.6 and later). In these cases, consider creating a custom policy (-M option).\n") out.info("Note: instructions for hardening targets, which correspond to the above policies, can be found at: \n") out.write() diff --git a/test/test_policy.py b/test/test_policy.py index cca9e3e..8ec3c6e 100644 --- a/test/test_policy.py +++ b/test/test_policy.py @@ -52,6 +52,14 @@ class TestPolicy: version_str = " (version %s)" % BUILTIN_POLICIES[policy_name]['version'] assert policy_name.endswith(version_str) + # Ensure version field is a string, but can be parsed as an integer. + version_field = BUILTIN_POLICIES[policy_name]['version'] + assert type(version_field) is str + try: + int(version_field) + except ValueError: + assert False, "version field of %s policy is not parseable as an integer." % policy_name + # Ensure no extra fields are present. assert len(required_fields) == len(BUILTIN_POLICIES[policy_name])