Build Nessus tracker workbook generator and docs

This commit is contained in:
andersen committed 2026-02-24 17:54:54 -05:00
commit da5f08bc1c
6 files changed
+510

No files matched your search

+62
View File
@@ -0,0 +1,62 @@
# AGENTS.md
## Purpose
This file defines agent behavior, contribution standards, and validation rules for this repository.
## Scope
- Applies to the entire repository unless a deeper `AGENTS.md` overrides it.
## Repository Baseline
- Source of truth is `scripts/build_tracker_workbook.py`.
- `nessus_remediation_tracker.xlsx` is a generated artifact and must be regenerated when workbook logic changes.
- Keep workbook logic compatible with both Excel and Google Sheets formulas.
- Keep dependency documentation in `README.md` accurate when Python module usage changes.
## File Standards
- Use Unix `LF` line endings for all text files.
- Preserve existing style and formatting conventions.
- Keep config examples free of real secrets.
- Prefer concise docs that describe operator workflow.
## Change Boundaries
- Keep edits minimal and directly tied to the request.
- Do not modify unrelated files or behavior.
- Preserve backward compatibility unless explicitly approved.
## Git Workflow
- Default flow: `git status` -> `git add` -> `git commit` -> `git push`.
- Use non-interactive git commands.
- Commit messages should be imperative and specific (example: `Use openpyxl to generate tracker workbook`).
- If `git push` fails, retry one additional time before escalating.
- Do not amend commits unless explicitly requested.
## Changelog Policy
- Keep a top-level `CHANGELOG.md`.
- Update changelog for any user-visible behavior change, generated workbook structure change, or workflow change.
- New entries go at the top with date (`YYYY-MM-DD`) and short bullets.
## Validation
- Run checks for changed areas.
- Minimum validation for workbook changes:
- `python3 scripts/build_tracker_workbook.py`
- `python3 -m py_compile scripts/build_tracker_workbook.py`
- `unzip -t nessus_remediation_tracker.xlsx`
- If a check cannot run, state exactly why in the final summary.
- Report exact validation commands run in the final summary.
## Safety
- Do not run destructive commands (for example `rm -rf` or `git reset --hard`) unless explicitly requested.
- Ask before high-impact or irreversible operations.
- Never overwrite unrelated user changes.
## Documentation
- Update `README.md` when behavior, configuration, or workflows change.
- Ensure docs match current script dependencies and usage.
- If dependency requirements change, update both installation commands (`apt` and `pip`) in `README.md`.
## Output Expectations
- Final summaries should include:
- files changed
- behavior impact
- validation performed
- risks or follow-up actions
+25
View File
@@ -0,0 +1,25 @@
# Changelog
## 2026-02-24
- Switched workbook generation to `openpyxl` in `scripts/build_tracker_workbook.py`.
- Kept `Remediations` and `Template` structure and matching logic (`CVE` first, then `Plugin ID`).
- Added repository baseline directives in `AGENTS.md` for git workflow, changelog updates, and workbook validation.
- Updated `README.md` with dependency and generation instructions.
- Updated template formulas so rows stay blank when both `CVE` and `Plugin ID` are empty.
- Updated template formulas so `NEW ITEM` appears only when an entered `CVE`/`Plugin ID` is unmatched.
- Made `Template` `Name` cells hyperlink to the matched `Remediations` row.
- Added `Severity` and `Status` dropdown validations on `Remediations`.
- Added severity/status conditional formatting on both `Remediations` and `Template`.
- Switched `Template` lookup flow to key-driven input (manual `Key` in column A).
- Added key normalization for lookup (`CVE:...`, `PID:...`, or raw value).
- Auto-populated both `CVE` and `Plugin ID` in `Template` when key match is found.
- Updated auto-populated `Template` CVE display to show `n/a` when a matched remediation row has no CVE value.
- Protected `Template` sheet so only `Key`, `Hosts`, and `Owner` cells are editable in data rows.
- Updated template scan note formulas to preserve blanks from `Remediations` instead of displaying `0`.
- Seeded the generator with the current `Remediations` entries as default test data.
- Enabled word wrap for Name/Description/notes columns in `Remediations` and Name/notes columns in `Template`.
- Switched workbook write to atomic temp-file replacement to prevent partially written/corrupt `.xlsx` outputs.
- Expanded status options to exception/in-progress variants and removed `nofix`.
- Updated status conditional formatting rules to color any `exception*` as blue, any `in progress*` as yellow, and `resolved` as green.
- Updated `README.md` dependency requirements and install instructions (`apt` + `pip`).
- Updated `AGENTS.md` to require dependency documentation updates when module requirements change.
+72
View File
@@ -0,0 +1,72 @@
# Nessus Tracker Workbook
This repo generates an `.xlsx` workbook for tracking Nessus remediation items.
## Dependencies
- Python `>=3.10`
- Python module: `openpyxl` (required)
- Python module: `xlsxwriter` (optional, not currently used by the generator)
Install on Debian/Ubuntu:
```bash
sudo apt install -y python3-openpyxl python3-xlsxwriter
```
Install with pip (alternative):
```bash
python3 -m pip install --user openpyxl xlsxwriter
```
## What it creates
- `Remediations` tab with columns:
- `CVE`, `Plugin ID`, `Severity`, `Name`, `Description`, `Links`, `Status`, `scan 1 notes`, `scan 2 notes`, `scan 3 notes`
- `Template` tab with columns:
- `Key`, `CVE`, `Plugin ID`, `Severity`, `Name`, `Hosts`, `Owner`, `status`, `scan 1 notes`, `scan 2 notes`, `scan 3 notes`
`Template` behavior:
- Enter `Key` in each row.
- `Key` may be:
1. `CVE:...`
2. `PID:...`
3. raw CVE/plugin text (no prefix)
- The sheet normalizes the key and matches against `Remediations` (`CVE` first, then `Plugin ID`).
- If `Key` is blank, formula-driven fields remain blank.
- If `Key` is entered and no match is found, fields show `NEW ITEM`.
- If matched, both `CVE` and `Plugin ID` are auto-populated in `Template`.
- If matched, `Name` in `Template` is a hyperlink to the matching row in `Remediations`.
- `Hosts` and `Owner` are intended for manual entry.
- `Template` is protected so only `Key`, `Hosts`, and `Owner` are editable (rows 2+).
`Remediations` controls:
- `Severity` has a dropdown: `info`, `low`, `medium`, `high`, `critical`.
- `Status` has a dropdown:
- `exception - 90 days`
- `exception - 6 months`
- `exception - permanent`
- `exception - nofix available`
- `In progress - next updates`
- `In progress - researching`
- `resolved`
- Conditional colors are applied for severity/status values.
## Generate workbook
```bash
python3 scripts/build_tracker_workbook.py
```
Output file: `nessus_remediation_tracker.xlsx`
## Validate workbook output
```bash
python3 -m py_compile scripts/build_tracker_workbook.py
python3 scripts/build_tracker_workbook.py
unzip -t nessus_remediation_tracker.xlsx
```
Binary file not shown.
+351
View File
@@ -0,0 +1,351 @@
#!/usr/bin/env python3
"""Generate the Nessus remediation tracker workbook."""
from __future__ import annotations
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",
"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 - nofix available",
"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",
"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",
"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",
"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 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_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 - 90 days,exception - 6 months,exception - permanent,exception - nofix available,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}")
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),
)
def build_remediations_sheet(wb: Workbook) -> None:
ws = wb.active
ws.title = "Remediations"
headers = [
"CVE",
"Plugin ID",
"Severity",
"Name",
"Description",
"Links",
"Status",
"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:J1"
set_column_widths(
ws,
{
"A": 20,
"B": 14,
"C": 10,
"D": 35,
"E": 55,
"F": 40,
"G": 16,
"H": 35,
"I": 35,
"J": 35,
},
)
set_wrap_text(ws, ["D", "E", "H", "I", "J"], 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",
"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:K1"
set_column_widths(
ws,
{
"A": 18,
"B": 20,
"C": 14,
"D": 12,
"E": 35,
"F": 22,
"G": 20,
"H": 16,
"I": 28,
"J": 28,
"K": 28,
"L": 12,
},
)
ws.column_dimensions["L"].hidden = True
ws.protection.sheet = True
ws.protection.autoFilter = True
set_wrap_text(ws, ["E", "I", "J", "K"], 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({normalized_key},Remediations!$A:$A,0),'
f'IFERROR(MATCH({normalized_key},Remediations!$B:$B,0),"")))'
)
ws[f"L{row}"] = lookup_formula
ws[f"B{row}"] = (
f'=IF(OR(A{row}="",$L{row}=""),"",'
f'IF(OR(INDEX(Remediations!$A:$A,$L{row})="",INDEX(Remediations!$A:$A,$L{row})=0),'
f'"n/a",INDEX(Remediations!$A:$A,$L{row})))'
)
ws[f"C{row}"] = f'=IF(OR(A{row}="",$L{row}=""),"",INDEX(Remediations!$B:$B,$L{row}))'
ws[f"D{row}"] = (
f'=IF(A{row}="","",'
f'IF($L{row}="","NEW ITEM",INDEX(Remediations!$C:$C,$L{row})))'
)
ws[f"E{row}"] = (
f'=IF(A{row}="","",'
f'IF($L{row}="","NEW ITEM",HYPERLINK("#\'Remediations\'!D"&$L{row},'
f'INDEX(Remediations!$D:$D,$L{row}))))'
)
ws[f"H{row}"] = (
f'=IF(A{row}="","",'
f'IF($L{row}="","NEW ITEM",INDEX(Remediations!$G:$G,$L{row})))'
)
ws[f"I{row}"] = (
f'=IF(OR(A{row}="",$L{row}=""),"",'
f'IF(INDEX(Remediations!$H:$H,$L{row})="","",INDEX(Remediations!$H:$H,$L{row})))'
)
ws[f"J{row}"] = (
f'=IF(OR(A{row}="",$L{row}=""),"",'
f'IF(INDEX(Remediations!$I:$I,$L{row})="","",INDEX(Remediations!$I:$I,$L{row})))'
)
ws[f"K{row}"] = (
f'=IF(OR(A{row}="",$L{row}=""),"",'
f'IF(INDEX(Remediations!$J:$J,$L{row})="","",INDEX(Remediations!$J:$J,$L{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()