File: test_profile_stability.py

package info (click to toggle)
scap-security-guide 0.1.78-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 114,600 kB
  • sloc: xml: 245,305; sh: 84,381; python: 33,093; makefile: 27
file content (144 lines) | stat: -rw-r--r-- 5,159 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
from __future__ import print_function

import argparse
import fnmatch
import json
import os.path
import sys

from tests.common import stability


def describe_change(difference, name):
    msg = ""

    msg += stability.describe_changeset(
        "Following selections were added to the {name} profile:\n".format(name=name),
        difference.added,
    )
    msg += stability.describe_changeset(
        "Following selections were removed from the {name} profile:\n".format(name=name),
        difference.removed,
    )
    return msg.rstrip()


def compare_sets(reference, sample):
    reference = set(reference)
    sample = set(sample)

    result = stability.Difference()
    result.added = list(sample.difference(reference))
    result.removed = list(reference.difference(sample))
    return result


def get_references_filenames(ref_root):
    found = []
    for root, dirs, files in os.walk(ref_root):
        for basename in files:
            if fnmatch.fnmatch(basename, "*.profile"):
                filename = os.path.join(root, basename)
                found.append(filename)
    return found


def corresponding_product_built(build_dir, reference_fname):
    ref_path_components = reference_fname.split(os.path.sep)
    product_id = ref_path_components[-2]
    return os.path.isdir(os.path.join(build_dir, product_id))


def get_matching_compiled_profile_filename(build_dir, reference_fname):
    ref_path_components = reference_fname.split(os.path.sep)
    product_id = ref_path_components[-2]
    profile_fname = ref_path_components[-1]
    matching_filename = os.path.join(build_dir, product_id, "profiles", profile_fname)
    if os.path.isfile(matching_filename):
        return matching_filename


def get_selections_key_from_json(json_fname):
    with open(json_fname, "r") as json_file:
        data = json.load(json_file)
        return data["selections"]


def get_reference_selections(reference_fname):
    with open(reference_fname, "r") as f:
        selections = f.read().splitlines()
    if selections != sorted(selections):
        msg = (
            f"The selections in the reference profile {reference_fname} are "
            f"not sorted. Please sort them before running this script.")
        raise RuntimeError(msg)
    return selections


def get_profile_name_from_reference_filename(fname):
    path_components = fname.split(os.path.sep)
    product_id = path_components[-2]
    profile_id = os.path.splitext(path_components[-1])[0]
    name = "{product_id}'s {profile_id}".format(
        product_id=product_id, profile_id=profile_id)
    return name


def get_reference_vs_built_difference(reference_fname, built_fname):
    ref_selections = get_reference_selections(reference_fname)
    built_selections = get_selections_key_from_json(built_fname)
    difference = compare_sets(ref_selections, built_selections)
    return difference


def inform_and_append_fix_based_on_reference_compiled_profile(ref, build_root, fix_commands):
    if not corresponding_product_built(build_root, ref):
        return

    compiled_profile = get_matching_compiled_profile_filename(build_root, ref)
    if not compiled_profile:
        msg = ("Unexpectedly unable to find compiled profile corresponding"
               "to the test file {ref}, although the corresponding product has been built. "
               "This indicates that a profile we have tests for is missing."
               .format(ref=ref))
        raise RuntimeError(msg)
    difference = get_reference_vs_built_difference(ref, compiled_profile)
    if not difference.empty:
        comprehensive_profile_name = get_profile_name_from_reference_filename(ref)
        stability.report_comparison(comprehensive_profile_name, difference, describe_change)
        fix_commands.append(
            "jq -r '.selections|sort[]' < '{compiled}' > '{reference}'"
            .format(compiled=compiled_profile, reference=ref)
        )


def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("build_root")
    parser.add_argument("test_data_root")
    args = parser.parse_args()

    reference_files = get_references_filenames(args.test_data_root)
    if not reference_files:
        raise RuntimeError("Unable to find any reference profiles in {test_root}"
                           .format(test_root=args.test_data_root))
    fix_commands = []
    for ref in reference_files:
        inform_and_append_fix_based_on_reference_compiled_profile(
                ref, args.build_root, fix_commands)

    if fix_commands:
        msg = (
            "If changes to mentioned profiles are intentional, "
            "update the profile stability data, so they become the new reference:\n{fixes}\n"
            "Please remember that if you change a profile that is extended by other profiles, "
            "changes propagate to derived profiles. "
            "If those changes are unwanted, you have to supress them "
            "using explicit selections or !unselections in derived profiles."
            .format(fixes="\n".join(fix_commands)))
        print(msg, file=sys.stderr)
    sys.exit(bool(fix_commands))


if __name__ == "__main__":
    main()