Files
nessus_tracker/scripts/build_tracker_workbook.py
T

403 lines
14 KiB
Python

#!/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<TODAY())'], fill=red),
)
def add_remediations_validation_and_formatting(ws) -> 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()