@@ -76,24 +76,58 @@ def _parse_scan_files(self, scan_files_str: str) -> List[str]:
7676 return [f .strip () for f in scan_files_str .split (',' ) if f .strip ()]
7777
7878 def get_scan_targets (self ) -> List [str ]:
79- """Determine files to scan based on configuration"""
80- # If explicit 'scan_all' set, return workspace directory
79+ """Determine files to scan based on configuration.
80+
81+ Precedence (highest to lowest):
82+ 1. ``scan_all`` -> scan the entire workspace (explicit override).
83+ 2. ``changed_files`` -> scope the scan to the PR/diff changed files
84+ (diff-only mode; mirrors how Socket SCA Pull Request alerts behave).
85+ 3. ``scan_files`` -> explicit user-provided file list.
86+ 4. default -> scan the entire workspace.
87+
88+ For the scoped modes (2 and 3) an empty list may be returned when none
89+ of the requested paths exist (for example a delete-only PR). Callers
90+ MUST treat an empty result as "nothing to scan" and skip the scanner
91+ rather than falling back to scanning the whole workspace or their own
92+ working directory.
93+ """
94+ # Explicit "scan everything" override.
8195 if self .get ('scan_all' , False ):
8296 return [str (self .workspace )]
8397
84- # If user provided specific files to scan, validate their existence
98+ # Diff-only mode: scope the scan to the files changed in the PR/commit.
99+ # Keep honoring the scope when git resolves to zero files, e.g. a
100+ # delete-only PR, so callers skip instead of scanning the workspace.
101+ changed_files = self .get ('changed_files' , []) or []
102+ if changed_files or self .get ('changed_files_scope_requested' , False ):
103+ return self ._resolve_file_targets (changed_files )
104+
105+ # Explicit list of files to scan.
85106 if self .scan_files :
86- targets = [self .workspace / f for f in self .scan_files ]
87- valid = []
88- for t in targets :
89- if t .exists ():
90- valid .append (str (t ))
91- else :
92- logging .getLogger (__name__ ).warning ("Scan target does not exist: %s" , str (t ))
93- return valid
94-
95- # Default: scan the workspace itself
107+ return self ._resolve_file_targets (self .scan_files )
108+
109+ # Default: scan the workspace itself.
96110 return [str (self .workspace )]
111+
112+ def _resolve_file_targets (self , files : List [str ]) -> List [str ]:
113+ """Resolve a list of file paths to absolute scan targets.
114+
115+ Relative paths are resolved against the workspace; absolute paths are
116+ used as-is. Paths that do not exist are skipped with a warning (a
117+ common case for delete-only PRs). Returns an empty list when none of
118+ the provided paths exist, signalling callers that there is nothing to
119+ scan.
120+ """
121+ valid : List [str ] = []
122+ for f in files :
123+ p = Path (f )
124+ if not p .is_absolute ():
125+ p = self .workspace / f
126+ if p .exists ():
127+ valid .append (str (p ))
128+ else :
129+ logging .getLogger (__name__ ).warning ("Scan target does not exist: %s" , str (p ))
130+ return valid
97131
98132 def get_action_for_severity (self , severity : str ) -> str :
99133 """Map severity to action according to security policy"""
@@ -1096,7 +1130,10 @@ def add_dynamic_cli_args(parser: argparse.ArgumentParser):
10961130
10971131 # Add optional changed-files CLI argument to limit scans to changed files
10981132 parser .add_argument ('--changed-files' , type = str , default = '' ,
1099- help = "Comma-separated list of files to scan or 'auto' to detect changed files from git" )
1133+ help = "Scope all scanners (SAST/OpenGrep, secrets, containers) to changed "
1134+ "files only. Accepts a comma-separated file list, a commit hash, "
1135+ "'auto' (PR base-ref diff in CI, else staged changes), 'pr' (diff "
1136+ "against GITHUB_BASE_REF), or 'current-commit'." )
11001137
11011138 # Also add CLI args for notification plugins declared in notifications.yaml
11021139 try :
@@ -1303,17 +1340,30 @@ def create_config_from_args(args) -> Config:
13031340 except Exception :
13041341 pass
13051342
1306- # Handle changed-files: CLI overrides env/config. Accept 'auto' to detect via git
1343+ # Handle changed-files: CLI overrides env/config. Accept 'auto' to detect via git.
1344+ # When invoked via the GitHub Action (entrypoint passes no CLI args) the value
1345+ # arrives through the INPUT_CHANGED_FILES environment variable instead.
13071346 changed_files_arg = getattr (args , 'changed_files' , '' ) if args is not None else ''
1347+ if not changed_files_arg :
1348+ changed_files_arg = os .getenv ('INPUT_CHANGED_FILES' , '' )
13081349 if changed_files_arg :
13091350 val = str (changed_files_arg ).strip ()
1310- # 'auto' defaults to staged changes (--cached)
1351+ config_dict ['changed_files_scope_requested' ] = True
1352+ # 'auto' resolves to the PR base-ref diff in CI, else staged changes.
13111353 if val .lower () == 'auto' :
13121354 try :
1313- git_changed = _detect_git_changed_files (config_dict .get ('workspace' , os .getcwd ()), mode = 'staged ' )
1355+ git_changed = _detect_git_changed_files (config_dict .get ('workspace' , os .getcwd ()), mode = 'auto ' )
13141356 config_dict ['changed_files' ] = git_changed
13151357 except Exception as e :
1316- logging .getLogger (__name__ ).warning ("Warning: failed to detect git changed files (staged): %s" , e )
1358+ logging .getLogger (__name__ ).warning ("Warning: failed to detect git changed files (auto): %s" , e )
1359+ config_dict ['changed_files' ] = []
1360+ elif val .lower () == 'pr' :
1361+ # Explicit PR diff against the base branch (GITHUB_BASE_REF).
1362+ try :
1363+ git_changed = _detect_git_changed_files (config_dict .get ('workspace' , os .getcwd ()), mode = 'pr' )
1364+ config_dict ['changed_files' ] = git_changed
1365+ except Exception as e :
1366+ logging .getLogger (__name__ ).warning ("Warning: failed to detect git changed files (pr): %s" , e )
13171367 config_dict ['changed_files' ] = []
13181368 elif val .lower () in ('current-commit' , 'current_commit' ):
13191369 try :
@@ -1370,27 +1420,34 @@ def create_config_from_args(args) -> Config:
13701420 return Config (config_dict )
13711421
13721422
1373- def _detect_git_changed_files (workspace_path : str , mode : str = 'staged' , commit : str | None = None ) -> List [str ]:
1423+ def _detect_git_changed_files (workspace_path : str , mode : str = 'staged' , commit : str | None = None , base_ref : str | None = None ) -> List [str ]:
13741424 """Detect changed files in a git repository.
13751425
13761426 mode:
1377- - 'staged' -> files staged for commit (git diff --name-only --cached)
1378- - 'current-commit' -> files included in HEAD commit
1379- - 'commit' -> files included in the given commit hash (commit param required)
1380-
1381- Returns a list of file paths relative to the workspace root. If not a git repo or detection fails, returns [].
1427+ - 'staged' -> files staged for commit (git diff --name-only --cached)
1428+ - 'current-commit' -> files included in the HEAD commit
1429+ - 'commit' -> files included in the given commit hash (commit param required)
1430+ - 'pr' -> files changed relative to a base ref (a GitHub PR).
1431+ Uses ``base_ref`` or ``GITHUB_BASE_REF`` and excludes
1432+ deletions so removed paths never become scan targets.
1433+ - 'auto' -> the PR base-ref diff when running in a PR CI context
1434+ (``GITHUB_BASE_REF`` is set), otherwise staged changes.
1435+ This is what ``--changed-files auto`` resolves to.
1436+
1437+ Returns a list of file paths relative to the workspace root. If not a git
1438+ repo or detection fails, returns [].
13821439 """
13831440 try :
13841441 from subprocess import check_output , CalledProcessError
13851442 import subprocess
1386-
1443+
13871444 # Prefer GITHUB_WORKSPACE if set (GitHub Actions environment)
13881445 # Otherwise use the provided workspace_path
13891446 if os .environ .get ('GITHUB_WORKSPACE' ):
13901447 ws = Path (os .environ ['GITHUB_WORKSPACE' ])
13911448 else :
13921449 ws = Path (workspace_path ) if workspace_path else Path .cwd ()
1393-
1450+
13941451 if not ws .exists ():
13951452 return []
13961453
@@ -1404,24 +1461,61 @@ def _detect_git_changed_files(workspace_path: str, mode: str = 'staged', commit:
14041461 original_cwd = os .getcwd ()
14051462 try :
14061463 os .chdir (str (ws ))
1407-
1408- if mode == 'staged' :
1464+
1465+ def _split (out : str ) -> List [str ]:
1466+ return [line .strip () for line in out .splitlines () if line .strip ()]
1467+
1468+ def _diff_against_base (ref : str ) -> Optional [List [str ]]:
1469+ """Diff changed files (excluding deletions) against a base ref.
1470+
1471+ Tries the remote-tracking ref (``origin/<ref>``) first, then the
1472+ bare ref. Returns None when neither ref can be resolved so the
1473+ caller can fall back to another detection strategy. The
1474+ ``--diff-filter=ACMR`` excludes deleted paths so they never
1475+ become scan targets.
1476+ """
1477+ if not ref :
1478+ return None
1479+ for candidate in (f'origin/{ ref } ' , ref ):
1480+ try :
1481+ out = check_output (
1482+ ['git' , 'diff' , '--name-only' , '--diff-filter=ACMR' , f'{ candidate } ...HEAD' ],
1483+ text = True , stderr = subprocess .DEVNULL ,
1484+ )
1485+ return _split (out )
1486+ except CalledProcessError :
1487+ continue
1488+ return None
1489+
1490+ if mode == 'auto' :
1491+ # Prefer the PR base-ref diff in CI; fall back to staged changes
1492+ # for local/pre-commit use.
1493+ base = base_ref or os .environ .get ('GITHUB_BASE_REF' , '' )
1494+ pr_files = _diff_against_base (base )
1495+ if pr_files is not None :
1496+ return pr_files
1497+ out = check_output (['git' , 'diff' , '--name-only' , '--cached' ], text = True , stderr = subprocess .DEVNULL )
1498+ return _split (out )
1499+ elif mode == 'pr' :
1500+ base = base_ref or os .environ .get ('GITHUB_BASE_REF' , '' )
1501+ return _diff_against_base (base ) or []
1502+ elif mode == 'staged' :
14091503 # staged but not yet committed
14101504 out = check_output (['git' , 'diff' , '--name-only' , '--cached' ], text = True , stderr = subprocess .DEVNULL )
1505+ return _split (out )
14111506 elif mode == 'current-commit' :
14121507 # files that are part of HEAD commit
14131508 out = check_output (['git' , 'diff-tree' , '--no-commit-id' , '--name-only' , '-r' , 'HEAD' ], text = True , stderr = subprocess .DEVNULL )
1509+ return _split (out )
14141510 elif mode == 'commit' and commit :
14151511 out = check_output (['git' , 'diff-tree' , '--no-commit-id' , '--name-only' , '-r' , commit ], text = True , stderr = subprocess .DEVNULL )
1512+ return _split (out )
14161513 else :
14171514 return []
1418-
1419- files = [line .strip () for line in out .splitlines () if line .strip ()]
1420- return files
14211515 finally :
14221516 # Always restore original working directory
14231517 os .chdir (original_cwd )
1424-
1518+
14251519 except CalledProcessError :
14261520 return []
14271521 except Exception :
0 commit comments