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 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205
|
#!/usr/bin/python3
import argparse
import difflib
import os.path
import jinja2
from openpyxl import load_workbook
from openpyxl.worksheet.worksheet import Worksheet
from srg_utils.worksheet_utils import Row, get_stigid_set, get_cce_dict_to_row_dict
from srg_utils import get_rule_dir_json, get_cce_dict, get_full_name
import ssg.jinja
SSG_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
BUILD_ROOT = os.path.join(SSG_ROOT, "build")
OUTPUT_PATH = os.path.join(BUILD_ROOT, "srg_diff.html")
RULES_JSON = os.path.join(BUILD_ROOT, "rule_dirs.json")
def _parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument('--base', '-b', help="The file to compare to, usually the file from the"
" latest build of CaC, in xlsx format.",
required=True)
parser.add_argument('--target', '-t', help="The file with the changes, usually the file "
"modified by external parities, in xlsx format.",
required=True)
parser.add_argument('--changed-name', '-n', type=str, action="store",
help="The name that DISA uses for the product. Defaults to RHEL 9",
default="RHEL 9")
parser.add_argument('--product', '-p', type=str, action="store", required=True,
help="The product")
parser.add_argument('--output', '-o', type=str, action="store", default=OUTPUT_PATH,
help=f"What file to output the diff to. Defaults to {OUTPUT_PATH}")
parser.add_argument("-r", "--root", type=str, action="store", default=SSG_ROOT,
help=f"Path to SSG root directory (defaults to {SSG_ROOT})")
parser.add_argument("-e", '--end-row', type=int, action="store", default=600,
help="What row to end on, defaults to 600")
parser.add_argument("-j", "--json", type=str, action="store", default=RULES_JSON,
help=f"Path to the rules_dir.json (defaults to {RULES_JSON})")
return parser.parse_args()
class SrgDiffResult:
cci = ""
rule_id = ""
Requirement = ""
Vul_Discussion = ""
Status = ""
Check = ""
Fix = ""
Severity = ""
def should_display(self):
return self.Requirement != "" or self.Vul_Discussion != "" or self.Status != "" or \
self.Check != "" or self.Fix != "" or self.Severity != ""
def __repr__(self):
return f'SrgDiffResult({self.cci}, {self.rule_id})'
def __lt__(self, other):
return self.cci < other.cci
def word_by_word_diff(original: str, edited: str) -> str:
original = clean_lines(original)
edited = clean_lines(edited)
if original is None or edited is None:
return ""
differ = difflib.HtmlDiff()
table = differ.make_table(clean_lines(original).split('\n'), edited.split('\n'))
return table
def clean_lines(lines: str) -> str:
result = list()
lines = lines.replace('>', '>')
lines = lines.replace('<', '<')
for line in lines.split('\n'):
result.append(line.rstrip())
return '\n'.join(result)
def _get_delta(cac: Row, disa: Row, cce: str, cce_rule_id_dict: dict) -> SrgDiffResult:
delta = SrgDiffResult()
delta.cci = cce
delta.rule_id = cce_rule_id_dict[cce]
if clean_lines(disa.Requirement) != cac.Requirement:
delta.Requirement = word_by_word_diff(disa.Requirement, cac.Requirement)
if clean_lines(disa.Vul_Discussion) != clean_lines(cac.Vul_Discussion):
delta.Vul_Discussion = word_by_word_diff(disa.Vul_Discussion, cac.Vul_Discussion)
if clean_lines(disa.Status) != clean_lines(cac.Status):
delta.Status = word_by_word_diff(disa.Status, cac.Status)
if clean_lines(disa.Check) != clean_lines(cac.Check):
delta.Check = word_by_word_diff(disa.Check, cac.Check)
if clean_lines(disa.Fix) != clean_lines(cac.Fix) and cac.Fix is not None and \
disa.Fix is not None:
delta.Fix = word_by_word_diff(disa.Fix, cac.Fix)
if clean_lines(disa.Severity) != cac.Severity:
disa.Severity = word_by_word_diff(disa.Severity, cac.Severity)
return delta
def _create_template(root_path: str) -> jinja2.Template:
loader = ssg.jinja.AbsolutePathFileSystemLoader()
env = jinja2.Environment(loader=loader)
path = os.path.join(root_path, 'utils', 'srg_diff.html')
template = env.get_template(path)
return template
def get_requirements_with_no_cces(sheet: Worksheet, end_row: int) -> list:
result = list()
for i in range(2, end_row):
requirement = sheet[f'F{i}'].value
if requirement is None or requirement.strip() == "":
continue
cce = sheet[f'D{i}'].value
if cce is not None and cce.startswith('CCE-') and requirement.strip() != "":
continue
status = sheet[f'I{i}'].value
if status is not None and status.strip() == 'Applicable - Configurable':
srgs_ids = sheet[f'C{i}'].value
result.append(f'{requirement.strip()} - {srgs_ids}')
return result
def get_deltas(cac_cce_dict: dict, cce_rule_id_dict: dict, common_set: set, disa_cce_dict: dict) \
-> list:
deltas = list()
for cce in common_set:
disa = disa_cce_dict[cce]
cac = cac_cce_dict[cce]
delta = _get_delta(cac, disa, cce, cce_rule_id_dict)
deltas.append(delta)
return deltas
def get_worksheet(path: str) -> Worksheet:
wb = load_workbook(path)
return wb['Sheet']
def get_missing_in(in_set: set, cce_rule_id_dict: dict) -> list:
result = list()
for cce in in_set:
cce = cce.replace('\n', '').strip()
result.append(f"{cce} - {cce_rule_id_dict[cce]}")
return result
def main():
args = _parse_args()
base_path = args.base
target_path = args.target
target_sheet = get_worksheet(base_path)
base_sheet = get_worksheet(target_path)
base_set = get_stigid_set(base_sheet, args.end_row)
target_set = get_stigid_set(target_sheet, args.end_row)
full_name = get_full_name(args.root, args.product)
cac_cce_dict = get_cce_dict_to_row_dict(target_sheet, full_name, args.changed_name,
args.end_row)
disa_cce_dict = get_cce_dict_to_row_dict(base_sheet, full_name, args.changed_name,
args.end_row)
base_missing_stig_ids = get_requirements_with_no_cces(base_sheet, args.end_row)
target_missing_stig_ids = get_requirements_with_no_cces(target_sheet, args.end_row)
common_set = target_set - (target_set - base_set)
rule_dir_json = get_rule_dir_json(args.json)
cce_rule_id_dict = get_cce_dict(rule_dir_json, args.product)
missing_in_base = get_missing_in((target_set - base_set), cce_rule_id_dict)
missing_in_target = get_missing_in((base_set - target_set), cce_rule_id_dict)
deltas = get_deltas(cac_cce_dict, cce_rule_id_dict, common_set, disa_cce_dict)
title = f"{base_path} vs {target_path}"
template = _create_template(args.root)
missing_in_base.sort()
missing_in_target.sort()
deltas.sort()
base_missing_stig_ids.sort()
target_missing_stig_ids.sort()
output = template.render(missing_in_base=missing_in_base, deltas=deltas,
missing_in_target=missing_in_target, title=title,
base_missing_stig_ids=base_missing_stig_ids,
target_missing_stig_ids=target_missing_stig_ids)
with open(args.output, 'w') as f:
f.write(output)
print(f"Wrote output to {args.output}.")
if __name__ == "__main__":
main()
|