66from socketsecurity import __version__
77from socketdev import INTEGRATION_TYPES , IntegrationType
88import json
9+ import tomllib
910
1011
1112def get_plugin_config_from_env (prefix : str ) -> dict :
@@ -15,6 +16,45 @@ def get_plugin_config_from_env(prefix: str) -> dict:
1516 except json .JSONDecodeError :
1617 return {}
1718
19+
20+ def load_cli_config_file (config_path : str ) -> dict :
21+ """
22+ Load CLI defaults from a JSON or TOML file.
23+
24+ Supported structures:
25+ - Flat keys: {"sarif_scope": "full", "reach": true}
26+ - Namespaced keys:
27+ - JSON: {"socketcli": {...}}
28+ - TOML: [socketcli]
29+ """
30+ if not config_path :
31+ return {}
32+
33+ try :
34+ with open (config_path , "rb" ) as f :
35+ if config_path .lower ().endswith (".json" ):
36+ data = json .loads (f .read ().decode ("utf-8" ))
37+ elif config_path .lower ().endswith (".toml" ):
38+ data = tomllib .load (f )
39+ else :
40+ logging .error ("--config must be a .json or .toml file" )
41+ exit (1 )
42+ except FileNotFoundError :
43+ logging .error (f"Config file not found: { config_path } " )
44+ exit (1 )
45+ except (json .JSONDecodeError , tomllib .TOMLDecodeError ) as e :
46+ logging .error (f"Invalid config file format: { e } " )
47+ exit (1 )
48+
49+ if not isinstance (data , dict ):
50+ logging .error ("Config file must contain a top-level object/table" )
51+ exit (1 )
52+
53+ scoped = data .get ("socketcli" )
54+ if isinstance (scoped , dict ):
55+ return scoped
56+ return data
57+
1858@dataclass
1959class PluginConfig :
2060 enabled : bool = False
@@ -42,6 +82,9 @@ class CliConfig:
4282 enable_sarif : bool = False
4383 sarif_file : Optional [str ] = None
4484 sarif_reachable_only : bool = False
85+ sarif_scope : str = "diff"
86+ sarif_grouping : str = "instance"
87+ sarif_reachability : str = "all"
4588 enable_gitlab_security : bool = False
4689 gitlab_security_file : Optional [str ] = None
4790 disable_overview : bool = False
@@ -90,10 +133,26 @@ class CliConfig:
90133 reach_use_only_pregenerated_sboms : bool = False
91134 max_purl_batch_size : int = 5000
92135 enable_commit_status : bool = False
136+ config_file : Optional [str ] = None
93137
94138 @classmethod
95139 def from_args (cls , args_list : Optional [List [str ]] = None ) -> 'CliConfig' :
96140 parser = create_argument_parser ()
141+
142+ pre_parser = argparse .ArgumentParser (add_help = False )
143+ pre_parser .add_argument ("--config" , dest = "config_file" , default = None )
144+ pre_args , _ = pre_parser .parse_known_args (args_list )
145+
146+ if pre_args .config_file :
147+ config_defaults = load_cli_config_file (pre_args .config_file )
148+ valid_dests = {action .dest for action in parser ._actions if action .dest != "help" }
149+ normalized_defaults = {}
150+ for key , value in config_defaults .items ():
151+ dest = str (key ).replace ("-" , "_" )
152+ if dest in valid_dests :
153+ normalized_defaults [dest ] = value
154+ parser .set_defaults (** normalized_defaults )
155+
97156 args = parser .parse_args (args_list )
98157
99158 # Get API token from env or args (check multiple env var names)
@@ -109,6 +168,13 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
109168 if args .sarif_file :
110169 args .enable_sarif = True
111170
171+ # Backward-compatible shim: --sarif-reachable-only => --sarif-reachability reachable
172+ if args .sarif_reachable_only :
173+ if args .sarif_reachability not in ("all" , "reachable" ):
174+ logging .error ("--sarif-reachable-only conflicts with --sarif-reachability" )
175+ exit (1 )
176+ args .sarif_reachability = "reachable"
177+
112178 # Strip quotes from commit message if present
113179 commit_message = args .commit_message
114180 if commit_message and commit_message .startswith ('"' ) and commit_message .endswith ('"' ):
@@ -134,6 +200,9 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
134200 'enable_sarif' : args .enable_sarif ,
135201 'sarif_file' : args .sarif_file ,
136202 'sarif_reachable_only' : args .sarif_reachable_only ,
203+ 'sarif_scope' : args .sarif_scope ,
204+ 'sarif_grouping' : args .sarif_grouping ,
205+ 'sarif_reachability' : args .sarif_reachability ,
137206 'enable_gitlab_security' : args .enable_gitlab_security ,
138207 'gitlab_security_file' : args .gitlab_security_file ,
139208 'disable_overview' : args .disable_overview ,
@@ -176,12 +245,20 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
176245 'reach_use_only_pregenerated_sboms' : args .reach_use_only_pregenerated_sboms ,
177246 'max_purl_batch_size' : args .max_purl_batch_size ,
178247 'enable_commit_status' : args .enable_commit_status ,
248+ 'config_file' : args .config_file ,
179249 'version' : __version__
180250 }
181- try :
182- config_args ["excluded_ecosystems" ] = json .loads (config_args ["excluded_ecosystems" ].replace ("'" , '"' ))
183- except json .JSONDecodeError :
184- logging .error (f"Unable to parse excluded_ecosystems: { config_args ['excluded_ecosystems' ]} " )
251+ excluded_ecosystems = config_args ["excluded_ecosystems" ]
252+ if isinstance (excluded_ecosystems , list ):
253+ config_args ["excluded_ecosystems" ] = excluded_ecosystems
254+ elif isinstance (excluded_ecosystems , str ):
255+ try :
256+ config_args ["excluded_ecosystems" ] = json .loads (excluded_ecosystems .replace ("'" , '"' ))
257+ except json .JSONDecodeError :
258+ logging .error (f"Unable to parse excluded_ecosystems: { excluded_ecosystems } " )
259+ exit (1 )
260+ else :
261+ logging .error (f"Unable to parse excluded_ecosystems: { excluded_ecosystems } " )
185262 exit (1 )
186263 # Build Slack plugin config, merging CLI arg with env config
187264 slack_config = get_plugin_config_from_env ("SOCKET_SLACK" )
@@ -216,6 +293,18 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
216293 if args .sarif_reachable_only and not args .reach :
217294 logging .error ("--sarif-reachable-only requires --reach to be specified" )
218295 exit (1 )
296+ if args .sarif_scope == "full" and not args .reach :
297+ logging .error ("--sarif-scope full requires --reach to be specified" )
298+ exit (1 )
299+ if args .sarif_reachability != "all" and not args .reach :
300+ logging .error ("--sarif-reachability requires --reach to be specified" )
301+ exit (1 )
302+ if args .sarif_grouping == "alert" and args .sarif_scope != "full" :
303+ logging .error ("--sarif-grouping alert currently requires --sarif-scope full" )
304+ exit (1 )
305+ if args .sarif_reachability in ("potentially" , "reachable-or-potentially" ) and args .sarif_scope != "full" :
306+ logging .error ("--sarif-reachability potentially/reachable-or-potentially requires --sarif-scope full" )
307+ exit (1 )
219308
220309 # Validate that only_facts_file requires reach
221310 if args .only_facts_file and not args .reach :
@@ -250,6 +339,12 @@ def create_argument_parser() -> argparse.ArgumentParser:
250339
251340 # Authentication
252341 auth_group = parser .add_argument_group ('Authentication' )
342+ auth_group .add_argument (
343+ "--config" ,
344+ dest = "config_file" ,
345+ metavar = "<path>" ,
346+ help = "Path to JSON/TOML file with default CLI options. CLI flags take precedence."
347+ )
253348 auth_group .add_argument (
254349 "--api-token" ,
255350 dest = "api_token" ,
@@ -497,6 +592,27 @@ def create_argument_parser() -> argparse.ArgumentParser:
497592 action = "store_true" ,
498593 help = "Filter SARIF output to only include reachable findings (requires --reach)"
499594 )
595+ output_group .add_argument (
596+ "--sarif-scope" ,
597+ dest = "sarif_scope" ,
598+ choices = ["diff" , "full" ],
599+ default = "diff" ,
600+ help = "Scope SARIF output to diff alerts (default) or full reachability facts data (requires --reach)"
601+ )
602+ output_group .add_argument (
603+ "--sarif-grouping" ,
604+ dest = "sarif_grouping" ,
605+ choices = ["instance" , "alert" ],
606+ default = "instance" ,
607+ help = "SARIF result grouping mode: instance (default) or alert (full scope only)"
608+ )
609+ output_group .add_argument (
610+ "--sarif-reachability" ,
611+ dest = "sarif_reachability" ,
612+ choices = ["all" , "reachable" , "potentially" , "reachable-or-potentially" ],
613+ default = "all" ,
614+ help = "Reachability filter for SARIF output (requires --reach when not 'all')"
615+ )
500616 output_group .add_argument (
501617 "--enable-gitlab-security" ,
502618 dest = "enable_gitlab_security" ,
@@ -756,4 +872,4 @@ def create_argument_parser() -> argparse.ArgumentParser:
756872 version = f'%(prog)s { __version__ } '
757873 )
758874
759- return parser
875+ return parser
0 commit comments