Add admin page overhaul and compliance schema drift check specs, compliance upload improvements, drift checker helper
This commit is contained in:
BIN
backend/scripts/__pycache__/extract_xlsx_schema.cpython-312.pyc
Normal file
BIN
backend/scripts/__pycache__/extract_xlsx_schema.cpython-312.pyc
Normal file
Binary file not shown.
44
backend/scripts/compliance_config.json
Normal file
44
backend/scripts/compliance_config.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"metric_categories": {
|
||||
"1.1.1": "Logging & Monitoring",
|
||||
"1.1.3": "Logging & Monitoring",
|
||||
"1.4.1": "Logging & Monitoring",
|
||||
"2.3.4i": "Vulnerability Management",
|
||||
"2.3.6i": "Vulnerability Management",
|
||||
"2.3.8i": "Vulnerability Management",
|
||||
"5.2.4": "Access & MFA",
|
||||
"5.2.5": "Access & MFA",
|
||||
"5.2.6": "Access & MFA",
|
||||
"5.2.7": "Access & MFA",
|
||||
"5.2.8": "Access & MFA",
|
||||
"5.3.4": "Endpoint Protection",
|
||||
"5.5.4i": "Vulnerability Management",
|
||||
"5.5.5": "Decommissioned Assets",
|
||||
"5.8.1": "Application Security",
|
||||
"7.1.1": "Logging & Monitoring",
|
||||
"7.1.4": "Logging & Monitoring",
|
||||
"7.6.13": "Disaster Recovery",
|
||||
"7.6.16": "Disaster Recovery",
|
||||
"Missing_AppID": "Asset Data Quality",
|
||||
"Missing_DF": "Asset Data Quality",
|
||||
"Missing_OS": "Asset Data Quality",
|
||||
"5.5.2": "Other"
|
||||
},
|
||||
"core_cols": [
|
||||
"Preferred - Hostname",
|
||||
"GRANITE - IPv4_Address",
|
||||
"GRANITE - Type",
|
||||
"Team",
|
||||
"Compliant",
|
||||
"Source_Network",
|
||||
"Vertical",
|
||||
"GRANITE - Equip_Inst_ID",
|
||||
"GRANITE - RESPONSIBLE_TEAM"
|
||||
],
|
||||
"skip_sheets": [
|
||||
"Summary",
|
||||
"CMDB_9box",
|
||||
"Vulns",
|
||||
"Aging Dashboard"
|
||||
]
|
||||
}
|
||||
91
backend/scripts/extract_xlsx_schema.py
Normal file
91
backend/scripts/extract_xlsx_schema.py
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Extract the structural schema of a compliance xlsx file as JSON.
|
||||
Usage: python3 extract_xlsx_schema.py <path_to_xlsx>
|
||||
|
||||
Output:
|
||||
{
|
||||
"sheets": [
|
||||
{
|
||||
"name": "Summary",
|
||||
"columns": ["Metric", "Non-Compliant", "..."],
|
||||
"metric_values": ["2.3.4i", "5.2.4", "..."]
|
||||
},
|
||||
{
|
||||
"name": "2.3.4i",
|
||||
"columns": ["Preferred - Hostname", "GRANITE - IPv4_Address", "..."]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
- Uses openpyxl in read-only mode.
|
||||
- Extracts sheet names, first-row column headers per sheet, and unique metric
|
||||
values from the Summary sheet (header at row 4, data from row 5 onward).
|
||||
- On error, returns { "error": "..." } on stdout and exits with non-zero code.
|
||||
|
||||
Dependencies: openpyxl (already in requirements.txt)
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
from openpyxl import load_workbook
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print(json.dumps({"error": "No file path provided"}))
|
||||
sys.exit(1)
|
||||
|
||||
filepath = sys.argv[1]
|
||||
|
||||
try:
|
||||
wb = load_workbook(filepath, read_only=True, data_only=True)
|
||||
except Exception as e:
|
||||
print(json.dumps({"error": f"Cannot open file: {str(e)}"}))
|
||||
sys.exit(1)
|
||||
|
||||
if not wb.sheetnames:
|
||||
print(json.dumps({"error": "Workbook contains no sheets"}))
|
||||
wb.close()
|
||||
sys.exit(1)
|
||||
|
||||
sheets = []
|
||||
for sheet_name in wb.sheetnames:
|
||||
ws = wb[sheet_name]
|
||||
|
||||
# Extract first-row column headers
|
||||
rows = list(ws.iter_rows(max_row=1, values_only=True))
|
||||
columns = [str(c).strip() for c in rows[0] if c is not None] if rows else []
|
||||
|
||||
entry = {
|
||||
"name": sheet_name,
|
||||
"columns": columns,
|
||||
}
|
||||
|
||||
# Extract metric values from the Summary sheet
|
||||
# Summary has header at row 4, data from row 5 onward
|
||||
if sheet_name == "Summary":
|
||||
metric_values = []
|
||||
header_rows = list(ws.iter_rows(min_row=4, max_row=4, values_only=True))
|
||||
if header_rows:
|
||||
summary_cols = [str(c).strip() if c else "" for c in header_rows[0]]
|
||||
metric_idx = None
|
||||
for i, col in enumerate(summary_cols):
|
||||
if col == "Metric":
|
||||
metric_idx = i
|
||||
break
|
||||
if metric_idx is not None:
|
||||
for row in ws.iter_rows(min_row=5, values_only=True):
|
||||
if row[metric_idx] is not None:
|
||||
val = str(row[metric_idx]).strip()
|
||||
if val and val != "Metric":
|
||||
metric_values.append(val)
|
||||
entry["metric_values"] = sorted(set(metric_values))
|
||||
|
||||
sheets.append(entry)
|
||||
|
||||
wb.close()
|
||||
print(json.dumps({"sheets": sheets}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -12,45 +12,35 @@ Output:
|
||||
}
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
|
||||
METRIC_CATEGORIES = {
|
||||
'2.3.4i': 'Vulnerability Management',
|
||||
'2.3.6i': 'Vulnerability Management',
|
||||
'2.3.8i': 'Vulnerability Management',
|
||||
'5.2.4': 'Access & MFA',
|
||||
'5.2.5': 'Access & MFA',
|
||||
'5.2.6': 'Access & MFA',
|
||||
'5.3.4': 'Endpoint Protection',
|
||||
'5.5.2': 'End-of-Life OS',
|
||||
'5.5.4i': 'Vulnerability Management',
|
||||
'5.5.5': 'Decommissioned Assets',
|
||||
'5.8.1': 'Application Security',
|
||||
'7.1.1': 'Logging & Monitoring',
|
||||
'7.6.13': 'Disaster Recovery',
|
||||
'7.6.16': 'Disaster Recovery',
|
||||
'1.1.1': 'Logging & Monitoring',
|
||||
'1.1.3': 'Logging & Monitoring',
|
||||
'1.4.1': 'Logging & Monitoring',
|
||||
'5.2.7': 'Access & MFA',
|
||||
'5.2.8': 'Access & MFA',
|
||||
'7.1.4': 'Logging & Monitoring',
|
||||
'Missing_AppID': 'Asset Data Quality',
|
||||
'Missing_DF': 'Asset Data Quality',
|
||||
'Missing_OS': 'Asset Data Quality',
|
||||
}
|
||||
|
||||
# Columns that go into the main item fields — everything else becomes extra_json
|
||||
CORE_COLS = {
|
||||
'Preferred - Hostname', 'GRANITE - IPv4_Address', 'GRANITE - Type',
|
||||
'Team', 'Compliant', 'Source_Network', 'Vertical',
|
||||
'GRANITE - Equip_Inst_ID', 'GRANITE - RESPONSIBLE_TEAM',
|
||||
}
|
||||
def load_config():
|
||||
"""Load parser configuration from compliance_config.json."""
|
||||
script_dir = os.path.dirname(os.path.abspath(__file__))
|
||||
config_path = os.path.join(script_dir, 'compliance_config.json')
|
||||
|
||||
SKIP_SHEETS = {'Summary', 'CMDB_9box', 'Vulns', 'Aging Dashboard'}
|
||||
try:
|
||||
with open(config_path, 'r') as f:
|
||||
config = json.load(f)
|
||||
except FileNotFoundError:
|
||||
print(f"Error: Configuration file not found: {config_path}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f"Error: Invalid JSON in configuration file {config_path}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
_config = load_config()
|
||||
METRIC_CATEGORIES = _config['metric_categories']
|
||||
CORE_COLS = set(_config['core_cols'])
|
||||
SKIP_SHEETS = set(_config['skip_sheets'])
|
||||
|
||||
|
||||
def safe_str(val):
|
||||
|
||||
Reference in New Issue
Block a user