Private access and GRC · Part 2 of 2

Turning a written control into a check I can run

I demonstrated GRC policies in the lab by giving them something I could check. Shell and Python turned each control into a repeatable test with an expected state, an observed state, and a follow-up.

I demonstrated governance, risk and compliance (GRC) policies in the lab by giving them something I could check. A written control such as “the DMZ cannot reach the admin network” is a promise. A script that tests it, records what it saw, and says what to follow up turns the promise into evidence.

I used shell scripting and Python for that active monitoring. The scripts and results below are examples with redacted aliases, and the report at the end is a simulated run.

From control to check

Control Read-only check Expected What I keep
Review privileged access Compare an exported admin group list with the approved list No unapproved members Additions, removals, approval reference
Detect missing endpoint logs Compare each endpoint’s last event time with its expected interval Every endpoint within its interval Stale endpoints, checked for collection health first
Verify DMZ isolation From a test host in the DMZ, try approved and blocked destinations Blocked destinations unreachable Source role, destination alias, port, time, expected and observed
Review incident actions Check each closed case for an owner, approval and disposition No gaps Cases routed back for follow-up

A shell check: does the DMZ stay put?

bashDMZ test host · unprivilegedExample output · substituted names and addresses

I wanted proof that the DMZ cannot open connections into the admin network, not just a diagram that says so.

while IFS=, read -r alias port; do
if ! getent hosts "$alias" >/dev/null; then
  printf '{"check":"dmz-%s-%s","error":"cannot resolve"}\n' "$alias" "$port"; continue
fi
if timeout 3 bash -c "</dev/tcp/$alias/$port" 2>/dev/null; then state=open; else state=closed; fi
printf '{"check":"dmz-%s-%s","observed":"%s","collected_at":"%s"}\n' "$alias" "$port" "$state" "$(date -u +%FT%TZ)"
done < blocked-destinations.csv > results.jsonl
Show output
{"check":"dmz-pve-admin.lab.example-8006","observed":"closed","collected_at":"2026-11-07T09:00:04Z"}
{"check":"dmz-soc-ui.lab.example-443","observed":"open","collected_at":"2026-11-07T09:00:07Z"}
{"check":"dmz-ops-ssh.lab.example-22","error":"cannot resolve"}

A name that does not resolve is a collection error, not proof that the path is blocked. The script records it separately so the report can say Unknown instead of a false pass.

A Python check: compare against the baseline

The Python side normalises what the shell scripts collected, compares each result with the policy baseline, and writes a timestamped report. Anything without fresh evidence is Unknown, never a pass.

#!/usr/bin/env python3
"""Compare collected check results with a policy baseline (Python 3.11+)."""
import json, sys
from datetime import datetime, timedelta, timezone

MAX_AGE = timedelta(hours=24)

def evaluate(expected: str, result: dict | None, now: datetime) -> tuple[str, str]:
    if not result or "error" in result:
        return "UNKNOWN", (result or {}).get("error", "no result collected")
    age = now - datetime.fromisoformat(result["collected_at"])
    if age > MAX_AGE:
        return "UNKNOWN", f"evidence is {age} old"
    if result["observed"] == expected:
        return "PASS", ""
    return "FAIL", f"expected {expected}, observed {result['observed']}"

def main(baseline_path: str, results_path: str) -> None:
    baseline = json.load(open(baseline_path))            # {"dmz-...-8006": "closed", ...}
    results = {r["check"]: r for r in map(json.loads, open(results_path))}
    now = datetime.now(timezone.utc)
    print(f"report generated {now:%Y-%m-%dT%H:%MZ}")
    for check, expected in baseline.items():
        status, note = evaluate(expected, results.get(check), now)
        print(f"{check:40} {status:8} {note}")

if __name__ == "__main__":
    main(*sys.argv[1:3])
report generated 2026-11-07T09:01Z                  (simulated run)
dmz-pve-admin.lab.example-8006           PASS
dmz-soc-ui.lab.example-443               FAIL     expected closed, observed open
dmz-ops-ssh.lab.example-22               UNKNOWN  cannot resolve

What happens after a FAIL

A failing check is the start of the work, not the end. In this simulated run, the DMZ test host could reach the SOC dashboard on 443. The follow-up is to find the Sophos rule that allowed it, decide whether the rule or the control is wrong, fix one of them, and run the check again. The report keeps all three runs, so the history shows the control was tested, broke, and was fixed.

The collection error matters just as much. If I had treated “cannot resolve” as closed, the report would have shown a pass for a path I never tested.

Takeaway: give every check three outcomes, PASS, FAIL and UNKNOWN, and never let UNKNOWN round up to PASS. A control is only as strong as the last time a check could actually see it.

Skills used

  • Turning a policy into a testable check
  • Reporting PASS, FAIL and UNKNOWN honestly
  • Bash and Python scripting

All skills, grouped by what job postings ask for

Tools in this entry

Every tool and its documentation