Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
59 changes: 32 additions & 27 deletions guards/github-guard/rust-guard/src/labels/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -940,7 +940,8 @@ pub(crate) fn extract_repo_from_github_url(url: &str) -> Option<String> {

/// Extract repository full name from a response item
/// Tries multiple fields in order: full_name, repository.full_name,
/// base.repo.full_name, head.repo.full_name, html_url parsing
/// base.repo.full_name, head.repo.full_name, then URL parsing from
/// repository_url, html_url, and url.
/// Returns empty string if no repo info found
pub fn extract_repo_from_item(item: &Value) -> String {
// Direct full_name (repositories)
Expand Down Expand Up @@ -973,23 +974,12 @@ pub fn extract_repo_from_item(item: &Value) -> String {
{
return name.to_string();
}
// repository_url parsing for search endpoints
if let Some(url) = item.get("repository_url").and_then(|v| v.as_str()) {
if let Some(repo_id) = extract_repo_from_github_url(url) {
return repo_id;
}
}
// html_url parsing as last resort - extract owner/repo from URLs like:
// https://github.com/owner/repo/pull/123 or https://github.com/owner/repo/issues/456
if let Some(url) = item.get("html_url").and_then(|v| v.as_str()) {
if let Some(repo_id) = extract_repo_from_github_url(url) {
return repo_id;
}
}
// Generic URL field fallback
if let Some(url) = item.get("url").and_then(|v| v.as_str()) {
if let Some(repo_id) = extract_repo_from_github_url(url) {
return repo_id;
// URL field fallback (repository_url for search results, html_url / url as generic fallbacks)
for field in &["repository_url", "html_url", "url"] {
if let Some(url) = item.get(field).and_then(|v| v.as_str()) {
if let Some(repo_id) = extract_repo_from_github_url(url) {
return repo_id;
}
}
}
Comment on lines +977 to 984
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

extract_repo_from_item now clearly falls back across repository_url, html_url, and url, but the function-level doc comment above still describes only html_url parsing. Please update the doc comment to match the actual fallback fields/order (and consider matching the existing extract_number_from_url loop style by avoiding the unnecessary *field deref for consistency).

Copilot uses AI. Check for mistakes.
String::new()
Expand Down Expand Up @@ -1278,11 +1268,7 @@ pub fn has_author_association(item: &Value) -> bool {
/// Users in the trusted_users list are also elevated to approved integrity.
pub fn author_association_floor(item: &Value, scope: &str, ctx: &PolicyContext) -> Vec<String> {
let author_login = extract_author_login(item);
if !author_login.is_empty()
&& (is_trusted_first_party_bot(author_login)
|| is_configured_trusted_bot(author_login, ctx)
|| is_trusted_user(author_login, ctx))
{
if !author_login.is_empty() && is_any_trusted_actor(author_login, ctx) {
return writer_integrity(scope, ctx);
}

Expand Down Expand Up @@ -1476,10 +1462,7 @@ pub fn pr_integrity(
);
// Elevate trusted bots and trusted users
let enriched_floor = if let Some(ref login) = facts.author_login {
if is_trusted_first_party_bot(login)
|| is_configured_trusted_bot(login, ctx)
|| is_trusted_user(login, ctx)
{
if is_any_trusted_actor(login, ctx) {
max_integrity(
repo_full_name,
enriched_floor,
Expand Down Expand Up @@ -1772,6 +1755,14 @@ pub fn is_trusted_user(username: &str, ctx: &PolicyContext) -> bool {
username_in_list(username, &ctx.trusted_users)
}

/// Returns `true` if `username` belongs to any trusted actor tier:
/// first-party bots, gateway-configured bots, or trusted users.
pub(crate) fn is_any_trusted_actor(username: &str, ctx: &PolicyContext) -> bool {
is_trusted_first_party_bot(username)
|| is_configured_trusted_bot(username, ctx)
|| is_trusted_user(username, ctx)
}
Comment on lines +1758 to +1764
Copy link

Copilot AI Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_any_trusted_actor is a new helper that centralizes a security-relevant trust decision and is now used at multiple call sites. Please add a small unit test covering the three tiers (first-party bot, configured trusted bot, trusted user) and a negative case to lock in the intended semantics and catch future omissions when new trust tiers are added.

Copilot uses AI. Check for mistakes.


#[cfg(test)]
mod tests {
Expand All @@ -1781,6 +1772,20 @@ mod tests {
PolicyContext::default()
}

#[test]
fn test_is_any_trusted_actor_tiers_and_negative() {
let ctx = PolicyContext {
trusted_bots: vec!["custom-bot".to_string()],
trusted_users: vec!["trusted-human".to_string()],
..Default::default()
};

assert!(is_any_trusted_actor("github-actions[bot]", &ctx));
assert!(is_any_trusted_actor("custom-bot", &ctx));
assert!(is_any_trusted_actor("trusted-human", &ctx));
assert!(!is_any_trusted_actor("random-user", &ctx));
}

#[test]
fn test_collaborator_permission_floor_admin() {
let ctx = test_ctx();
Expand Down
9 changes: 3 additions & 6 deletions guards/github-guard/rust-guard/src/labels/tool_rules.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ use super::helpers::{
author_association_floor_from_str,
elevate_via_collaborator_permission, ensure_integrity_baseline,
extract_number_as_string, extract_repo_info, extract_repo_info_from_search_query,
format_repo_id, is_configured_trusted_bot, is_default_branch_commit_context,
is_default_branch_ref, is_trusted_first_party_bot, is_trusted_user, max_integrity,
format_repo_id, is_any_trusted_actor, is_default_branch_commit_context,
is_default_branch_ref, max_integrity,
merged_integrity, policy_private_scope_label, private_user_label, project_github_label,
reader_integrity, writer_integrity, PolicyContext,
};
Expand Down Expand Up @@ -95,10 +95,7 @@ fn resolve_author_integrity(
let mut floor = author_association_floor_from_str(repo_id, author_association, ctx);

if let Some(login) = author_login {
if is_trusted_first_party_bot(login)
|| is_configured_trusted_bot(login, ctx)
|| is_trusted_user(login, ctx)
{
if is_any_trusted_actor(login, ctx) {
floor = max_integrity(repo_id, floor, writer_integrity(repo_id, ctx), ctx);
}
let resource_id = format!("{}/{}#{}", owner, repo, resource_num);
Expand Down
Loading