@@ -280,7 +280,7 @@ def parse_args() -> argparse.Namespace:
280280 p .add_argument ("--input-file" , help = "Batch file with one domain per line" )
281281 p .add_argument ("--expected-ns" , default = str (DEFAULT_EXPECTED_NS_FILE ), help = "Expected nameserver policy JSON" )
282282 p .add_argument ("--dkim-selectors" , default = str (DEFAULT_DKIM_SELECTORS_FILE ), help = "Per-domain DKIM selectors JSON" )
283- p .add_argument ("--output" , choices = ["json" ], default = "json" )
283+ p .add_argument ("--output" , choices = ["json" , "markdown" ], default = "json" )
284284 p .add_argument ("--version" , action = "version" , version = f"domain-security-monitor { VERSION } " )
285285 return p .parse_args ()
286286
@@ -306,6 +306,59 @@ def load_domains(args: argparse.Namespace) -> list[str]:
306306 return dedup
307307
308308
309+ def render_markdown (results : list [dict [str , Any ]]) -> str :
310+ lines = [
311+ "# Domain Security Monitor Report" ,
312+ "" ,
313+ f"Domains checked: **{ len (results )} **" ,
314+ "" ,
315+ ]
316+
317+ for item in results :
318+ domain = item .get ("domain" , "unknown" )
319+ generated = item .get ("generated_at_utc" , "" )
320+ signals = item .get ("signals" , {})
321+
322+ lines .append (f"## { domain } " )
323+ lines .append ("" )
324+ if generated :
325+ lines .append (f"- Generated (UTC): `{ generated } `" )
326+
327+ # quick summary counts
328+ status_counts = {"pass" : 0 , "warn" : 0 , "fail" : 0 , "unknown" : 0 }
329+ for sig in signals .values ():
330+ st = str ((sig or {}).get ("status" , "unknown" )).lower ()
331+ status_counts [st ] = status_counts .get (st , 0 ) + 1
332+ lines .append (
333+ "- Summary: "
334+ f"pass={ status_counts .get ('pass' , 0 )} , "
335+ f"warn={ status_counts .get ('warn' , 0 )} , "
336+ f"fail={ status_counts .get ('fail' , 0 )} , "
337+ f"unknown={ status_counts .get ('unknown' , 0 )} "
338+ )
339+ lines .append ("" )
340+
341+ lines .append ("| Signal | Status | Confidence | Data source |" )
342+ lines .append ("|---|---|---|---|" )
343+ for name , sig in signals .items ():
344+ sig = sig or {}
345+ lines .append (
346+ f"| `{ name } ` | { sig .get ('status' , 'unknown' )} | { sig .get ('confidence' , 'low' )} | { sig .get ('data_source' , 'unknown' )} |"
347+ )
348+
349+ lines .append ("" )
350+ lines .append ("<details>" )
351+ lines .append ("<summary>Signal details</summary>" )
352+ lines .append ("" )
353+ lines .append ("```json" )
354+ lines .append (json .dumps (signals , indent = 2 ))
355+ lines .append ("```" )
356+ lines .append ("</details>" )
357+ lines .append ("" )
358+
359+ return "\n " .join (lines ).rstrip () + "\n "
360+
361+
309362def main () -> int :
310363 args = parse_args ()
311364 domains = load_domains (args )
@@ -317,7 +370,11 @@ def main() -> int:
317370 dkim_cfg = load_json (Path (args .dkim_selectors ), {})
318371
319372 results = [analyse_domain (d , expected_cfg , dkim_cfg ) for d in domains ]
320- print (json .dumps ({"count" : len (results ), "results" : results }, indent = 2 ))
373+
374+ if args .output == "markdown" :
375+ print (render_markdown (results ), end = "" )
376+ else :
377+ print (json .dumps ({"count" : len (results ), "results" : results }, indent = 2 ))
321378 return 0
322379
323380
0 commit comments