66import argparse
77import json
88import sys
9+ from collections .abc import Mapping
910from pathlib import Path
10- from typing import Any
11+ from typing import Any , cast
12+
13+ from beartype import beartype
14+ from icontract import ensure
1115
1216
1317FindingKey = tuple [str , str , int ]
1418
1519
1620def _finding_key (result : dict [str , Any ]) -> FindingKey :
17- check_id = str (result .get ("check_id" , "" ))
18- path = str (result .get ("path" , "" ))
19- start_raw = result .get ("start" , {})
20- start_line = int (start_raw .get ("line" , 0 )) if isinstance (start_raw , dict ) else 0
21+ check_id = result .get ("check_id" )
22+ path = result .get ("path" )
23+ start = result .get ("start" )
24+ if not isinstance (check_id , str ) or not check_id .strip ():
25+ raise ValueError ("Semgrep result missing non-empty check_id" )
26+ if not isinstance (path , str ) or not path .strip ():
27+ raise ValueError ("Semgrep result missing non-empty path" )
28+ if not isinstance (start , dict ):
29+ raise ValueError ("Semgrep result missing integer start.line" )
30+ start_line = cast (Mapping [str , object ], start ).get ("line" )
31+ if not isinstance (start_line , int ):
32+ raise ValueError ("Semgrep result missing integer start.line" )
2133 return (check_id , path , start_line )
2234
2335
36+ def _emit_stdout (message : str ) -> None :
37+ sys .stdout .write (f"{ message } \n " )
38+
39+
2440def _load_json (path : Path ) -> dict [str , Any ]:
2541 try :
2642 payload = json .loads (path .read_text (encoding = "utf-8" ))
@@ -56,9 +72,20 @@ def _load_results(path: Path) -> list[dict[str, Any]]:
5672 raw_results = payload .get ("results" )
5773 if not isinstance (raw_results , list ):
5874 raise SystemExit (f"::error::{ path } missing Semgrep results list" )
59- return [result for result in raw_results if isinstance (result , dict )]
60-
61-
75+ results : list [dict [str , Any ]] = []
76+ for index , result in enumerate (raw_results ):
77+ if not isinstance (result , dict ):
78+ raise SystemExit (f"::error::{ path } results[{ index } ] must be an object" )
79+ try :
80+ _finding_key (result )
81+ except ValueError as exc :
82+ raise SystemExit (f"::error::{ path } results[{ index } ] is malformed: { exc } " ) from exc
83+ results .append (result )
84+ return results
85+
86+
87+ @beartype
88+ @ensure (lambda result : result in {0 , 1 }, "exit code must be 0 or 1" )
6289def main () -> int :
6390 parser = argparse .ArgumentParser (description = __doc__ )
6491 parser .add_argument ("--results" , type = Path , required = True , help = "Semgrep JSON results file" )
@@ -71,15 +98,15 @@ def main() -> int:
7198 new_findings = sorted (current - baseline )
7299 resolved_findings = sorted (baseline - current )
73100
74- print (f"Semgrep SAST findings: { len (current )} current, { len (baseline )} accepted baseline" )
101+ _emit_stdout (f"Semgrep SAST findings: { len (current )} current, { len (baseline )} accepted baseline" )
75102 if resolved_findings :
76- print (f"Semgrep SAST baseline can shrink by { len (resolved_findings )} finding(s)" )
103+ _emit_stdout (f"Semgrep SAST baseline can shrink by { len (resolved_findings )} finding(s)" )
77104 if not new_findings :
78- print ("Semgrep SAST gate passed: no new findings outside baseline" )
105+ _emit_stdout ("Semgrep SAST gate passed: no new findings outside baseline" )
79106 return 0
80107
81108 for check_id , path , line in new_findings :
82- print (f"::error file={ path } ,line={ line } ::New Semgrep SAST finding: { check_id } " )
109+ _emit_stdout (f"::error file={ path } ,line={ line } ::New Semgrep SAST finding: { check_id } " )
83110 return 1
84111
85112
0 commit comments