Companion checker for lint_control_mapping.sh — stdlib-only Markdown-table
parsing of an ATO control-mapping doc. Not a compliance scanner: this
validates that the table itself is complete, not that the controls are
Requires the table to have a control-ID column (NIST 800-53 IDs like AC-2,
AU-3, SC-7), an impact-level column, and an evidence column. Any row whose
verdict/status cell uses an accepted-state word (aligned, met, satisfied,
implemented, compliant) must have a non-empty, non-placeholder evidence cell.
python3 check_control_mapping.py FILE
Exit 0 if every accepted-state row has evidence and all three columns exist,
CONTROL_ID = re.compile(r"^[A-Z]{2}-\d{1,2}(\(\d+\))?$")
ACCEPTED_STATE_WORDS = re.compile(
r"\b(aligned|met|satisfied|implemented|compliant)\b", re.IGNORECASE
PLACEHOLDER_CELLS = {"", "-", "n/a", "na", "tbd", "todo"}
TABLE_ROW = re.compile(r'^\s*\|(.+)\|\s*$')
TABLE_SEP = re.compile(r'^\s*\|[\s:\-|]+\|\s*$')
if TABLE_SEP.match(line):
m = TABLE_ROW.match(line)
if header is not None and rows:
# a non-table line after we've started a table — table block ended
cells = [c.strip() for c in m.group(1).split('|')]
def col_idx(header, name_re):
for i, h in enumerate(header):
print("Usage: check_control_mapping.py FILE", file=sys.stderr)
with open(path, 'r') as f:
lines = f.read().splitlines()
print(f"ERROR: cannot read {path}: {e}", file=sys.stderr)
header, rows = parse_table(lines)
errors.append("No Markdown table found at all — expected a control-mapping table.")
control_col = col_idx(header, re.compile(r"control", re.IGNORECASE))
impact_col = col_idx(header, re.compile(r"impact", re.IGNORECASE))
evidence_col = col_idx(header, re.compile(r"evidence", re.IGNORECASE))
verdict_col = col_idx(header, re.compile(r"verdict|status|state", re.IGNORECASE))
missing_cols.append("control ID column")
missing_cols.append("impact-level column")
missing_cols.append("evidence column")
errors.append(f"Table is missing required column(s): {', '.join(missing_cols)}")
return row[i].strip() if i is not None and i < len(row) else ""
control_cell = cell(row, control_col)
m = CONTROL_ID.match(control_cell)
control_id = control_cell
# verdict may live in its own column, or be folded into the impact
# column text — check whichever column(s) exist for an accepted word.
verdict_text = cell(row, verdict_col) if verdict_col is not None else ""
verdict_text = " ".join(row)
is_accepted = bool(ACCEPTED_STATE_WORDS.search(verdict_text))
evidence_cell = cell(row, evidence_col)
has_evidence = evidence_cell.lower() not in PLACEHOLDER_CELLS
if is_accepted and not has_evidence:
f"control {control_id}: marked accepted-state "
f"({ACCEPTED_STATE_WORDS.search(verdict_text).group(0)!r}) "
f"with an empty/placeholder evidence cell"
oks.append(f"control {control_id}: accepted-state row has non-empty evidence")
oks.append(f"control {control_id}: not an accepted-state row, evidence not required")
"No row with a recognizable NIST 800-53 control ID (e.g. AC-2, SC-7) found in the table."
return 1 if errors else 0
if __name__ == '__main__':