#!/usr/bin/env python3 """Generate the Nessus remediation tracker workbook.""" from __future__ import annotations from datetime import date from pathlib import Path from openpyxl import Workbook from openpyxl.formatting.rule import FormulaRule from openpyxl.styles import Alignment from openpyxl.styles import Protection from openpyxl.styles import Font from openpyxl.styles import PatternFill from openpyxl.worksheet.datavalidation import DataValidation OUTPUT_FILE = Path("nessus_remediation_tracker.xlsx") MAX_TEMPLATE_ROWS = 1000 MAX_XLSX_ROWS = 1048576 FORMAT_MAX_ROWS = 5000 DEFAULT_REMEDIATIONS_ROWS = [ [ "CVE-2022-22278 ", 505201, "high", "SonicWALL SonicOS Allocation of Resources Without Limits or Throttling (CVE-2022-22278) ", "A vulnerability in SonicOS CFS (Content filtering service) returns a large 403 forbidden HTTP response message to the source address when users try to access prohibited resource this allows an attacker to cause HTTP Denial of Service (DoS) attack ", None, "resolved", None, date(2026, 1, 20), "test scan 1 cve-2022-22278", None, None, ], [ "CVE-2025-61726", 299887, "high", "RHEL 8 : grafana (RHSA-2026:3188)", "The net/url package does not set a limit on the number of query parameters in a query. While the maximum size of query parameters in URLs is generally limited by the maximum request header size, the net/http.Request.ParseForm method can parse large URL-encoded forms. Parsing a large form containing many unique query parameters can cause excessive memory consumption. ", "https://www.tenable.com/cve/CVE-2025-61726", "exception", date(2026, 3, 31), date(2026, 2, 12), "test scan 1 cve-2025-61726", "test scan 2 cve-612726", None, ], [ "CVE-2025-61728", 299887, "medium", "RHEL 8 : grafana (RHSA-2026:3188)", "archive/zip uses a super-linear file name indexing algorithm that is invoked the first time a file in an archive is opened. This can lead to a denial of service when consuming a maliciously constructed ZIP archive. ", "https://www.tenable.com/cve/CVE-2025-61728", "resolved", None, date(2026, 2, 5), "test scan 1 cve-2025-61728", "test scan 2 cve-61728", "test scan 3 cve-2025-61728", ], [ "CVE-2025-68121", 299887, "critical", "RHEL 8 : grafana (RHSA-2026:3188)", "During session resumption in crypto/tls, if the underlying Config has its ClientCAs or RootCAs fields mutated between the initial handshake and the resumed handshake, the resumed handshake may succeed when it should have failed. This may happen when a user calls Config.Clone and mutates the returned Config, or uses Config.GetConfigForClient. This can cause a client to resume a session with a server that it would not have resumed with during the initial handshake, or cause a server to resume a session with a client that it would not have resumed with during the initial handshake. ", "https://www.tenable.com/cve/CVE-2025-68121", "exception", date(2026, 2, 10), date(2026, 1, 10), "test scan 1 cve-2025-68121", None, "test scan 3 cve-2025-68121", ], [ None, 148367, "critical", "Python Unsupported Version Detection", "The remote host contains one or more unsupported versions of Python.", "https://www.tenable.com/plugins/nessus/148367", "exception - permanent", None, date(2025, 12, 12), "only applies to Python from the Python foundation, not the version maintained on AlmaLinux", None, None, ], ] def set_column_widths(ws, widths): for col, width in widths.items(): ws.column_dimensions[col].width = width def set_wrap_text(ws, columns: list[str], start_row: int, end_row: int) -> None: wrap_alignment = Alignment(wrap_text=True, vertical="top") for col in columns: for row in range(start_row, end_row + 1): ws[f"{col}{row}"].alignment = wrap_alignment def set_date_format(ws, columns: list[str], start_row: int, end_row: int) -> None: for col in columns: for row in range(start_row, end_row + 1): ws[f"{col}{row}"].number_format = "yyyy-mm-dd" def add_severity_rules(ws, cell_range: str) -> None: blue = PatternFill(start_color="FFDBEAFE", end_color="FFDBEAFE", fill_type="solid") yellow = PatternFill(start_color="FFFEF3C7", end_color="FFFEF3C7", fill_type="solid") orange = PatternFill(start_color="FFFED7AA", end_color="FFFED7AA", fill_type="solid") red = PatternFill(start_color="FFFECACA", end_color="FFFECACA", fill_type="solid") ws.conditional_formatting.add( cell_range, FormulaRule(formula=['OR(LOWER(C2)="info",LOWER(C2)="low")'], fill=blue), ) ws.conditional_formatting.add( cell_range, FormulaRule(formula=['LOWER(C2)="medium"'], fill=yellow), ) ws.conditional_formatting.add( cell_range, FormulaRule(formula=['LOWER(C2)="high"'], fill=orange), ) ws.conditional_formatting.add( cell_range, FormulaRule(formula=['OR(LOWER(C2)="critical",LOWER(C2)="red")'], fill=red), ) def add_status_rules(ws, cell_range: str) -> None: blue = PatternFill(start_color="FFDBEAFE", end_color="FFDBEAFE", fill_type="solid") yellow = PatternFill(start_color="FFFEF3C7", end_color="FFFEF3C7", fill_type="solid") green = PatternFill(start_color="FFDCFCE7", end_color="FFDCFCE7", fill_type="solid") ws.conditional_formatting.add( cell_range, FormulaRule(formula=['LEFT(LOWER(G2),9)="exception"'], fill=blue), ) ws.conditional_formatting.add( cell_range, FormulaRule(formula=['LEFT(LOWER(G2),11)="in progress"'], fill=yellow), ) ws.conditional_formatting.add( cell_range, FormulaRule(formula=['LOWER(G2)="resolved"'], fill=green), ) def add_expired_date_rule(ws, cell_range: str, date_col: str) -> None: red = PatternFill(start_color="FFFECACA", end_color="FFFECACA", fill_type="solid") ws.conditional_formatting.add( cell_range, FormulaRule(formula=[f'AND(ISNUMBER(${date_col}2),${date_col}2 None: severity_validation = DataValidation( type="list", formula1='"info,low,medium,high,critical"', allow_blank=True, ) ws.add_data_validation(severity_validation) severity_validation.add(f"C2:C{MAX_XLSX_ROWS}") status_validation = DataValidation( type="list", formula1='"exception,exception - permanent,In progress - next updates,In progress - researching,resolved"', allow_blank=True, ) ws.add_data_validation(status_validation) status_validation.add(f"G2:G{MAX_XLSX_ROWS}") add_severity_rules(ws, f"C2:C{MAX_XLSX_ROWS}") add_status_rules(ws, f"G2:G{MAX_XLSX_ROWS}") add_expired_date_rule(ws, f"H2:H{MAX_XLSX_ROWS}", "H") def add_template_formatting(ws, max_rows: int) -> None: blue = PatternFill(start_color="FFDBEAFE", end_color="FFDBEAFE", fill_type="solid") yellow = PatternFill(start_color="FFFEF3C7", end_color="FFFEF3C7", fill_type="solid") orange = PatternFill(start_color="FFFED7AA", end_color="FFFED7AA", fill_type="solid") red = PatternFill(start_color="FFFECACA", end_color="FFFECACA", fill_type="solid") green = PatternFill(start_color="FFDCFCE7", end_color="FFDCFCE7", fill_type="solid") ws.conditional_formatting.add( f"D2:D{max_rows}", FormulaRule(formula=['OR(LOWER(D2)="info",LOWER(D2)="low")'], fill=blue), ) ws.conditional_formatting.add( f"D2:D{max_rows}", FormulaRule(formula=['LOWER(D2)="medium"'], fill=yellow), ) ws.conditional_formatting.add( f"D2:D{max_rows}", FormulaRule(formula=['LOWER(D2)="high"'], fill=orange), ) ws.conditional_formatting.add( f"D2:D{max_rows}", FormulaRule(formula=['OR(LOWER(D2)="critical",LOWER(D2)="red")'], fill=red), ) ws.conditional_formatting.add( f"H2:H{max_rows}", FormulaRule(formula=['LEFT(LOWER(H2),9)="exception"'], fill=blue), ) ws.conditional_formatting.add( f"H2:H{max_rows}", FormulaRule(formula=['LEFT(LOWER(H2),11)="in progress"'], fill=yellow), ) ws.conditional_formatting.add( f"H2:H{max_rows}", FormulaRule(formula=['LOWER(H2)="resolved"'], fill=green), ) add_expired_date_rule(ws, f"I2:I{max_rows}", "I") def build_remediations_sheet(wb: Workbook) -> None: ws = wb.active ws.title = "Remediations" headers = [ "CVE", "Plugin ID", "Severity", "Name", "Description", "Links", "Status", "Excepted until (date)", "First seen", "scan 1 notes", "scan 2 notes", "scan 3 notes", ] ws.append(headers) for row in DEFAULT_REMEDIATIONS_ROWS: ws.append(row) for cell in ws[1]: cell.font = Font(bold=True) ws.freeze_panes = "A2" ws.auto_filter.ref = "A1:L1" set_column_widths( ws, { "A": 20, "B": 14, "C": 10, "D": 35, "E": 55, "F": 40, "G": 16, "H": 20, "I": 14, "J": 35, "K": 35, "L": 35, }, ) set_wrap_text(ws, ["D", "E", "J", "K", "L"], 2, FORMAT_MAX_ROWS) set_date_format(ws, ["H", "I"], 2, FORMAT_MAX_ROWS) add_remediations_validation_and_formatting(ws) def build_template_sheet(wb: Workbook, max_rows: int) -> None: ws = wb.create_sheet("Template") headers = [ "Key", "CVE", "Plugin ID", "Severity", "Name", "Hosts", "Owner", "status", "Excepted until (date)", "Unresolved for", "scan 1 notes", "scan 2 notes", "scan 3 notes", "_lookup_row", ] ws.append(headers) for cell in ws[1]: cell.font = Font(bold=True) ws.freeze_panes = "A2" ws.auto_filter.ref = "A1:M1" set_column_widths( ws, { "A": 18, "B": 20, "C": 14, "D": 12, "E": 35, "F": 22, "G": 20, "H": 16, "I": 20, "J": 16, "K": 28, "L": 28, "M": 28, "N": 12, }, ) ws.column_dimensions["N"].hidden = True ws.protection.sheet = True ws.protection.autoFilter = True set_wrap_text(ws, ["E", "K", "L", "M"], 2, max_rows) set_date_format(ws, ["I"], 2, max_rows) for row in range(2, max_rows + 1): ws[f"A{row}"].protection = Protection(locked=False) ws[f"F{row}"].protection = Protection(locked=False) ws[f"G{row}"].protection = Protection(locked=False) normalized_key = ( f'IF(LEFT(UPPER(A{row}),4)="CVE:",MID(A{row},5,999),' f'IF(LEFT(UPPER(A{row}),4)="PID:",MID(A{row},5,999),A{row}))' ) lookup_formula = ( f'=IF(A{row}="","",IFERROR(MATCH(1,INDEX((TRIM(Remediations!$A:$A)=TRIM({normalized_key}))*1,0),0),' f'IFERROR(MATCH(TRIM({normalized_key}),Remediations!$B:$B,0),"")))' ) ws[f"N{row}"] = lookup_formula ws[f"B{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(OR(INDEX(Remediations!$A:$A,$N{row})="",INDEX(Remediations!$A:$A,$N{row})=0),' f'"",INDEX(Remediations!$A:$A,$N{row})))' ) ws[f"C{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(OR(INDEX(Remediations!$B:$B,$N{row})="",INDEX(Remediations!$B:$B,$N{row})=0),"",' f'INDEX(Remediations!$B:$B,$N{row})))' ) ws[f"D{row}"] = ( f'=IF(A{row}="","",' f'IF($N{row}="","NEW ITEM",IF(INDEX(Remediations!$C:$C,$N{row})="","",INDEX(Remediations!$C:$C,$N{row}))))' ) ws[f"E{row}"] = ( f'=IF(A{row}="","",' f'IF($N{row}="","NEW ITEM",HYPERLINK("#\'Remediations\'!D"&$N{row},' f'IF(INDEX(Remediations!$D:$D,$N{row})="","",INDEX(Remediations!$D:$D,$N{row})))))' ) ws[f"H{row}"] = ( f'=IF(A{row}="","",' f'IF($N{row}="","NEW ITEM",IF(INDEX(Remediations!$G:$G,$N{row})="","",INDEX(Remediations!$G:$G,$N{row}))))' ) ws[f"I{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(OR(INDEX(Remediations!$H:$H,$N{row})="",INDEX(Remediations!$H:$H,$N{row})=0),"",' f'INDEX(Remediations!$H:$H,$N{row})))' ) ws[f"J{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(OR(INDEX(Remediations!$I:$I,$N{row})="",NOT(ISNUMBER(INDEX(Remediations!$I:$I,$N{row})))),"",' f'IF(TODAY()-INDEX(Remediations!$I:$I,$N{row})<=0,"",TODAY()-INDEX(Remediations!$I:$I,$N{row})&" days")))' ) ws[f"K{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(INDEX(Remediations!$J:$J,$N{row})="","",INDEX(Remediations!$J:$J,$N{row})))' ) ws[f"L{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(INDEX(Remediations!$K:$K,$N{row})="","",INDEX(Remediations!$K:$K,$N{row})))' ) ws[f"M{row}"] = ( f'=IF(OR(A{row}="",$N{row}=""),"",' f'IF(INDEX(Remediations!$L:$L,$N{row})="","",INDEX(Remediations!$L:$L,$N{row})))' ) add_template_formatting(ws, max_rows) def main() -> None: wb = Workbook() build_remediations_sheet(wb) build_template_sheet(wb, MAX_TEMPLATE_ROWS) temp_output = OUTPUT_FILE.with_suffix(".tmp.xlsx") wb.save(temp_output) temp_output.replace(OUTPUT_FILE) print(f"Wrote {OUTPUT_FILE} with Template rows 2..{MAX_TEMPLATE_ROWS}") if __name__ == "__main__": main()