Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions examples/.github-webhook-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -171,3 +171,11 @@ test-oracle:
- "tests/**/*.py"
triggers:
- approved

# Security Checks (overrides global config)
security-checks:
mandatory: true
suspicious-paths:
- ".github/workflows/"
- ".github/actions/"
committer-identity-check: true
Comment thread
myakove marked this conversation as resolved.
22 changes: 22 additions & 0 deletions examples/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,21 @@ ai-features:
enabled: true
timeout-minutes: 10 # Timeout in minutes for AI CLI (default: 10)

# Security Checks configuration
# Detects potentially malicious PR patterns like modifications to
# security-sensitive paths or mismatched committer identities.
security-checks:
mandatory: true # true (default) = blocks can-be-merged. false = advisory only
suspicious-paths:
- ".claude/"
- ".vscode/"
- ".cursor/"
- ".devcontainer/"
- ".pi/"
- ".github/workflows/"
- ".github/actions/"
committer-identity-check: true

Comment thread
myakove marked this conversation as resolved.
repositories:
my-repository:
name: my-org/my-repository
Expand Down Expand Up @@ -286,3 +301,10 @@ repositories:
- "tests/**/*.py"
triggers:
- approved

# Security Checks (overrides global)
# security-checks:
# suspicious-paths:
# - ".github/workflows/"
# - ".github/actions/"
# committer-identity-check: true
32 changes: 32 additions & 0 deletions webhook_server/config/schema.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,34 @@ $defs:
- ai-provider
- ai-model
additionalProperties: false
security-checks:
type: object
description: |
Security checks for pull requests. Detects potentially malicious PR patterns
such as modifications to security-sensitive paths or mismatched committer identities.
properties:
mandatory:
type: boolean
description: |
When true (default), security checks block can-be-merged.
When false, security checks are advisory only (run but don't block merge).
default: true
suspicious-paths:
type: array
description: |
List of path prefixes considered security-sensitive.
PRs modifying files under these paths will fail the security-suspicious-paths check run
and have auto-merge blocked.
Default: [".claude/", ".vscode/", ".cursor/", ".devcontainer/", ".pi/", ".github/workflows/", ".github/actions/"]
items:
type: string
committer-identity-check:
type: boolean
description: |
When enabled, compares the PR author (parent committer) against the last commit's
committer. Fails the security-committer-identity check run if they differ.
default: true
additionalProperties: false
type: object
properties:
log-level:
Expand Down Expand Up @@ -213,6 +241,8 @@ properties:
additionalProperties: false
ai-features:
$ref: '#/$defs/ai-features'
security-checks:
$ref: '#/$defs/security-checks'
labels:
type: object
description: Configure which labels are enabled and their colors
Expand Down Expand Up @@ -631,6 +661,8 @@ properties:
additionalProperties: false
ai-features:
$ref: '#/$defs/ai-features'
security-checks:
$ref: '#/$defs/security-checks'
custom-check-runs:
type: array
description: |
Expand Down
30 changes: 29 additions & 1 deletion webhook_server/libs/github_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
CAN_BE_MERGED_STR,
CONFIGURABLE_LABEL_CATEGORIES,
CONVENTIONAL_TITLE_STR,
DEFAULT_SUSPICIOUS_PATHS,
OTHER_MAIN_BRANCH,
PRE_COMMIT_STR,
PYTHON_MODULE_INSTALL_STR,
Expand Down Expand Up @@ -662,7 +663,7 @@ async def process(self) -> Any:

self.last_commit = await self._get_last_commit(pull_request=pull_request)
self.parent_committer = pull_request.user.login
self.last_committer = getattr(self.last_commit.committer, "login", self.parent_committer)
self.last_committer = getattr(self.last_commit.committer, "login", "unknown")

# Store PR SHAs: prefer webhook payload (avoids race condition with live API)
# For pull_request events, base.sha and head.sha are guaranteed by GitHub webhook spec.
Expand Down Expand Up @@ -953,6 +954,33 @@ def _repo_data_from_config(self, repository_config: dict[str, Any]) -> None:
self.ai_features: dict[str, Any] | None = self.config.get_value(
value="ai-features", return_on_none=None, extra_dict=repository_config
)
_security_checks: dict[str, Any] | None = self.config.get_value(
value="security-checks", return_on_none=None, extra_dict=repository_config
)
_security_config = _security_checks if isinstance(_security_checks, dict) else {}
_suspicious_paths = _security_config.get("suspicious-paths", DEFAULT_SUSPICIOUS_PATHS)
self.security_suspicious_paths: list[str] = (
[str(p).strip() for p in _suspicious_paths if isinstance(p, (str, int, float)) and str(p).strip()]
if isinstance(_suspicious_paths, list)
else DEFAULT_SUSPICIOUS_PATHS
)
Comment thread
myakove marked this conversation as resolved.
Comment thread
myakove marked this conversation as resolved.
_committer_check_raw = _security_config.get("committer-identity-check", True)
if not isinstance(_committer_check_raw, bool):
self.logger.warning(
f"{self.log_prefix} security-checks.committer-identity-check must be boolean, "
f"got {type(_committer_check_raw).__name__}. Defaulting to true."
)
_committer_check_raw = True
self.security_committer_identity_check: bool = _committer_check_raw
_mandatory_raw = _security_config.get("mandatory", True)
if not isinstance(_mandatory_raw, bool):
self.logger.warning(
f"{self.log_prefix} security-checks.mandatory must be boolean, got {type(_mandatory_raw).__name__}. "
"Defaulting to true."
)
_mandatory_raw = True
self.security_mandatory: bool = _mandatory_raw

_auto_merge_prs = self.config.get_value(
value="set-auto-merge-prs", return_on_none=[], extra_dict=repository_config
)
Expand Down
10 changes: 10 additions & 0 deletions webhook_server/libs/handlers/check_run_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
IN_PROGRESS_STR,
PYTHON_MODULE_INSTALL_STR,
QUEUED_STR,
SECURITY_COMMITTER_IDENTITY_STR,
SECURITY_SUSPICIOUS_PATHS_STR,
SUCCESS_STR,
TOX_STR,
VERIFIED_LABEL_STR,
Expand Down Expand Up @@ -427,6 +429,14 @@ async def all_required_status_checks(self, pull_request: PullRequest) -> list[st
check_name = custom_check["name"]
all_required_status_checks.append(check_name)

# Add mandatory security checks
if self.github_webhook.security_mandatory:
if self.github_webhook.security_suspicious_paths:
all_required_status_checks.append(SECURITY_SUSPICIOUS_PATHS_STR)

if self.github_webhook.security_committer_identity_check:
all_required_status_checks.append(SECURITY_COMMITTER_IDENTITY_STR)

# Use ordered deduplication to combine branch and config checks without duplicates
_all_required_status_checks = list(dict.fromkeys(branch_required_status_checks + all_required_status_checks))
self.logger.debug(f"{self.log_prefix} All required status checks: {_all_required_status_checks}")
Expand Down
64 changes: 63 additions & 1 deletion webhook_server/libs/handlers/issue_comment_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from github.PullRequest import PullRequest
from github.Repository import Repository

from webhook_server.libs.handlers.check_run_handler import CheckRunHandler
from webhook_server.libs.handlers.check_run_handler import CheckRunHandler, CheckRunOutput
from webhook_server.libs.handlers.labels_handler import LabelsHandler
from webhook_server.libs.handlers.owners_files_handler import OwnersFileHandler
from webhook_server.libs.handlers.pull_request_handler import PullRequestHandler
Expand All @@ -30,9 +30,12 @@
COMMAND_REGENERATE_WELCOME_STR,
COMMAND_REPROCESS_STR,
COMMAND_RETEST_STR,
COMMAND_SECURITY_OVERRIDE_STR,
COMMAND_TEST_ORACLE_STR,
HOLD_LABEL_STR,
REACTIONS,
SECURITY_COMMITTER_IDENTITY_STR,
SECURITY_SUSPICIOUS_PATHS_STR,
USER_LABELS_DICT,
VERIFIED_LABEL_STR,
WIP_STR,
Expand Down Expand Up @@ -171,6 +174,7 @@ async def user_commands(
COMMAND_ADD_ALLOWED_USER_STR,
COMMAND_REGENERATE_WELCOME_STR,
COMMAND_TEST_ORACLE_STR,
COMMAND_SECURITY_OVERRIDE_STR,
]

command_and_args: list[str] = command.split(" ", 1)
Expand Down Expand Up @@ -362,6 +366,64 @@ async def user_commands(
pull_request.create_issue_comment, msg, logger=self.logger, log_prefix=self.log_prefix
)

elif _command == COMMAND_SECURITY_OVERRIDE_STR:
maintainers = await self.owners_file_handler.get_all_repository_maintainers()
if reviewed_user not in maintainers:
msg = "Only maintainers can use `/security-override`"
self.logger.debug(f"{self.log_prefix} {msg}")
await github_api_call(
pull_request.create_issue_comment, body=msg, logger=self.logger, log_prefix=self.log_prefix
)
return

if remove:
# Re-run security checks to re-evaluate
await self.runner_handler.run_security_suspicious_paths()
await self.runner_handler.run_security_committer_identity()
self.logger.info(f"{self.log_prefix} Security checks re-run by {reviewed_user}")
await github_api_call(
pull_request.create_issue_comment,
body=f"Security override removed by @{reviewed_user}. Security checks re-run.",
logger=self.logger,
log_prefix=self.log_prefix,
)
else:
# Set security check runs to success (override)
if (
not self.github_webhook.security_suspicious_paths
and not self.github_webhook.security_committer_identity_check
):
msg = "No security checks are enabled — nothing to override."
self.logger.debug(f"{self.log_prefix} {msg}")
await github_api_call(
pull_request.create_issue_comment, body=msg, logger=self.logger, log_prefix=self.log_prefix
)
return

override_output: CheckRunOutput = {
"title": f"Overridden by maintainer @{reviewed_user}",
"summary": "Security check overridden by maintainer",
"text": (
f"This security check was overridden by maintainer @{reviewed_user}.\n\n"
"Use `/security-override cancel` to re-run security checks."
),
}
if self.github_webhook.security_suspicious_paths:
await self.check_run_handler.set_check_success(
name=SECURITY_SUSPICIOUS_PATHS_STR, output=override_output
)
if self.github_webhook.security_committer_identity_check:
await self.check_run_handler.set_check_success(
name=SECURITY_COMMITTER_IDENTITY_STR, output=override_output
)
self.logger.info(f"{self.log_prefix} Security override applied by {reviewed_user}")
await github_api_call(
pull_request.create_issue_comment,
body=f"Security checks overridden by @{reviewed_user}. Security check runs set to pass.",
logger=self.logger,
log_prefix=self.log_prefix,
)
Comment thread
myakove marked this conversation as resolved.

elif _command == WIP_STR:
if not await self.owners_file_handler.is_user_valid_to_run_commands(
pull_request=pull_request, reviewed_user=reviewed_user
Expand Down
Loading