From 2800b34d5448540faea0817dc0cb33326c8b4056 Mon Sep 17 00:00:00 2001 From: "Andoni A." <14891798+andoniaf@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:11:15 +0100 Subject: [PATCH] feat(github_actions): enhance workflow exclusion with full path support Zizmor doesn't support --exclude flag natively, so implemented exclusion at Prowler level by filtering findings after scan completes. The exclusion now supports both pattern types: - Filename patterns: 'test-*.yml' matches any file named test-*.yml - Full path patterns: '.github/workflows/test-*.yml' matches specific paths - Subdirectory patterns: '**/experimental/*.yml' matches subdirs Changes: - Added fnmatch import for glob pattern matching - Removed invalid --exclude flags from zizmor command - Implemented _should_exclude_workflow() with dual matching logic - Filter findings during processing based on exclusion patterns - Added comprehensive test covering all pattern types - Updated documentation with examples of both pattern types --- .../getting-started-github-actions.mdx | 23 ++++++++- .../github_actions/github_actions_provider.py | 46 +++++++++++++++-- .../test_github_actions_provider.py | 50 +++++++++++++++++++ 3 files changed, 113 insertions(+), 6 deletions(-) diff --git a/docs/user-guide/providers/github-actions/getting-started-github-actions.mdx b/docs/user-guide/providers/github-actions/getting-started-github-actions.mdx index 82dc2040b2..7c91dcdcd5 100644 --- a/docs/user-guide/providers/github-actions/getting-started-github-actions.mdx +++ b/docs/user-guide/providers/github-actions/getting-started-github-actions.mdx @@ -98,12 +98,31 @@ Authentication for private repositories can be provided using one of the followi ### Excluding Workflows -Exclude specific workflows or patterns from scanning: +Exclude specific workflows from the scan results using glob patterns. Prowler filters findings after zizmor completes the scan, matching patterns against both workflow filenames and full paths: ```bash -prowler github_actions --exclude-workflows "test-*.yml" "experimental/*" +# Exclude by filename pattern (matches any path) +prowler github_actions --exclude-workflows "test-*.yml" "api-*.yml" + +# Exclude by full path pattern +prowler github_actions --exclude-workflows ".github/workflows/experimental-*.yml" + +# Exclude entire subdirectories +prowler github_actions --exclude-workflows "**/draft/*.yml" + +# Combine multiple pattern types +prowler github_actions \ + --workflow-path .github/workflows \ + --exclude-workflows "test-*.yml" ".github/workflows/experimental/*" ``` + +Patterns can match against either the full path or just the filename: +- `test-*.yml` matches `test-deploy.yml` in any directory +- `.github/workflows/test-*.yml` matches only workflows in that specific directory +- `**/experimental/*.yml` matches workflows in any `experimental` subdirectory + + ### Output Formats The GitHub Actions provider supports all standard Prowler output formats: diff --git a/prowler/providers/github_actions/github_actions_provider.py b/prowler/providers/github_actions/github_actions_provider.py index 6a8ece4f2c..bc96f70690 100644 --- a/prowler/providers/github_actions/github_actions_provider.py +++ b/prowler/providers/github_actions/github_actions_provider.py @@ -3,6 +3,7 @@ import shutil import subprocess import sys import tempfile +from fnmatch import fnmatch from os import environ from typing import List @@ -132,6 +133,42 @@ class GithubActionsProvider(Provider): """GitHub Actions provider doesn't need a session since it uses zizmor directly""" return None + def _should_exclude_workflow(self, workflow_file: str, exclude_patterns: list[str]) -> bool: + """ + Check if a workflow file should be excluded based on exclude patterns. + + Patterns can match against either: + - The full workflow path (e.g., ".github/workflows/test-*.yml") + - Just the filename (e.g., "test-*.yml") + + Args: + workflow_file: The workflow file path + exclude_patterns: List of glob patterns to exclude + + Returns: + True if the workflow should be excluded, False otherwise + """ + if not exclude_patterns: + return False + + # Get just the filename from the path + from os.path import basename + filename = basename(workflow_file) + + # Check if the pattern matches either the full path or just the filename + for pattern in exclude_patterns: + # Try matching against full path first + if fnmatch(workflow_file, pattern): + logger.debug(f"Excluding workflow {workflow_file} (matches full path pattern: {pattern})") + return True + + # Also try matching against just the filename for convenience + if fnmatch(filename, pattern): + logger.debug(f"Excluding workflow {workflow_file} (matches filename pattern: {pattern})") + return True + + return False + def _extract_workflow_file_from_location(self, location: dict) -> str: """ Extract the workflow file path from a location object. @@ -368,6 +405,7 @@ class GithubActionsProvider(Provider): logger.info(f"Running GitHub Actions security scan on {directory} ...") # Build zizmor command + # Note: zizmor doesn't support --exclude, so we filter findings after scanning zizmor_command = [ "zizmor", directory, @@ -375,10 +413,6 @@ class GithubActionsProvider(Provider): "json", ] - # Add exclude patterns if provided - for exclude_pattern in exclude_workflows: - zizmor_command.extend(["--exclude", exclude_pattern]) - with alive_bar( ctrl_c=False, bar="blocks", @@ -444,6 +478,10 @@ class GithubActionsProvider(Provider): location ) if workflow_file: + # Check if this workflow should be excluded + if self._should_exclude_workflow(workflow_file, exclude_workflows): + continue + report = self._process_zizmor_finding( finding, workflow_file, location ) diff --git a/tests/providers/github_actions/test_github_actions_provider.py b/tests/providers/github_actions/test_github_actions_provider.py index 30109a3b7f..8761f8b623 100644 --- a/tests/providers/github_actions/test_github_actions_provider.py +++ b/tests/providers/github_actions/test_github_actions_provider.py @@ -222,3 +222,53 @@ class TestGithubActionsProvider: args = mock_print.call_args[0] assert "https://github.com/test/repo" in str(args[0]) assert "test*.yml" in str(args[0]) + + def test_should_exclude_workflow(self): + """Test workflow exclusion pattern matching""" + with patch.object(GithubActionsProvider, "setup_session", return_value=None): + provider = GithubActionsProvider() + + # Test no exclusions + assert not provider._should_exclude_workflow( + ".github/workflows/test.yml", [] + ) + + # Test exact filename match + assert provider._should_exclude_workflow( + ".github/workflows/test.yml", ["test.yml"] + ) + + # Test wildcard pattern on filename + assert provider._should_exclude_workflow( + ".github/workflows/test-api.yml", ["test-*.yml"] + ) + + # Test multiple patterns + assert provider._should_exclude_workflow( + ".github/workflows/api-test.yml", ["test-*.yml", "api-*.yml"] + ) + + # Test no match + assert not provider._should_exclude_workflow( + ".github/workflows/deploy.yml", ["test-*.yml", "api-*.yml"] + ) + + # Test full path matching + assert provider._should_exclude_workflow( + ".github/workflows/test.yml", [".github/workflows/test.yml"] + ) + + # Test full path with wildcard + assert provider._should_exclude_workflow( + ".github/workflows/api-tests.yml", [".github/workflows/api-*.yml"] + ) + + # Test subdirectory patterns + assert provider._should_exclude_workflow( + ".github/workflows/experimental/test.yml", ["**/experimental/*.yml"] + ) + + # Test that filename pattern works regardless of path + assert provider._should_exclude_workflow( + "workflows/subdir/test-deploy.yml", ["test-*.yml"] + )