Build Nessus tracker workbook generator and docs
This commit is contained in:
commit
da5f08bc1c
6 files changed
+510
No files matched your search
@@ -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
|
||||||
@@ -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.
|
||||||
@@ -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.
Binary file not shown.
@@ -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()
|
||||||
Reference in new issue
Block a user