Skip to content

Commit e35491b

Browse files
committed
Add SARIF scoping/reachability controls, config file support
Signed-off-by: lelia <lelia@socket.dev>
1 parent 4903ae3 commit e35491b

File tree

3 files changed

+507
-25
lines changed

3 files changed

+507
-25
lines changed

socketsecurity/config.py

Lines changed: 121 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
from socketsecurity import __version__
77
from socketdev import INTEGRATION_TYPES, IntegrationType
88
import json
9+
import tomllib
910

1011

1112
def 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
1959
class 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

Comments
 (0)