Skip to content

Commit 992acb3

Browse files
authored
fix(guard): cover 4 guard gaps — get_code_quality_finding, ui_get, add_gpg_key, add_ssh_key
1 parent 3ef2ed4 commit 992acb3

2 files changed

Lines changed: 197 additions & 0 deletions

File tree

guards/github-guard/rust-guard/src/labels/tool_rules.rs

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,14 @@ pub fn apply_tool_labels(
341341
integrity = writer_integrity(repo_id, ctx);
342342
}
343343

344+
// === Code quality findings (repo-scoped) ===
345+
// S = S(repo) — inherits from repository visibility
346+
// I = writer (requires repo write access to post/view code quality findings)
347+
"get_code_quality_finding" => {
348+
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
349+
integrity = writer_integrity(repo_id, ctx);
350+
}
351+
344352
// === Actions: Workflow/Artifact Metadata and Artifact Downloads ===
345353
"actions_get" => {
346354
let method = tool_args.get("method").and_then(|v| v.as_str()).unwrap_or("");
@@ -355,6 +363,33 @@ pub fn apply_tool_labels(
355363
integrity = writer_integrity(repo_id, ctx);
356364
}
357365

366+
// === UI metadata dispatch (repo/org-scoped, method-dependent) ===
367+
// Mirrors existing rules for list_label, list_branches, list_issue_types,
368+
// list_issue_fields, and list_repository_collaborators.
369+
"ui_get" => {
370+
let method = tool_args.get("method").and_then(|v| v.as_str()).unwrap_or("");
371+
match method {
372+
// Repo-scoped metadata: labels, milestones, branches
373+
// S = S(repo); I = writer
374+
"labels" | "milestones" | "branches" => {
375+
secrecy = apply_repo_visibility_secrecy(&owner, &repo, repo_id, secrecy, ctx);
376+
integrity = writer_integrity(repo_id, ctx);
377+
}
378+
// Org-level type/field definitions (GitHub-controlled metadata)
379+
// S = inherits from org; I = approved:github
380+
"issue_types" | "issue_fields" => {
381+
integrity = project_github_label(ctx);
382+
}
383+
// Access-sensitive membership/reviewer data
384+
// S = private policy scope; I = reader
385+
"assignees" | "reviewers" => {
386+
secrecy = policy_private_scope_label(&owner, &repo, repo_id, ctx);
387+
integrity = reader_integrity(repo_id, ctx);
388+
}
389+
_ => {}
390+
}
391+
}
392+
358393
// === Repo-scoped resources: visibility-inherited secrecy, approved integrity ===
359394
// S = inherits from repo visibility; I = approved (writer-level)
360395
"actions_list"
@@ -649,6 +684,19 @@ pub fn apply_tool_labels(
649684
integrity = writer_integrity(repo_id, ctx);
650685
}
651686

687+
// === User SSH/GPG key management (account-scoped writes) ===
688+
// Pre-emptive synthetic guard entries for CLI-only operations:
689+
// `gh ssh-key add` → POST /user/keys and /user/ssh_signing_keys
690+
// `gh gpg-key add` → POST /user/gpg_keys
691+
// Adding auth/signing keys is a high-risk account-level write operation.
692+
// S = private:user (user-account-scoped sensitive data)
693+
// I = writer(user) (requires authenticated account write access)
694+
"add_gpg_key" | "add_ssh_key" => {
695+
secrecy = private_user_label();
696+
baseline_scope = Cow::Borrowed(scope_names::USER);
697+
integrity = writer_integrity(scope_names::USER, ctx);
698+
}
699+
652700
// === Dynamic toolset enablement (capability expansion) ===
653701
"enable_toolset" => {
654702
// Enabling a toolset expands the agent's runtime capability set.
@@ -1297,4 +1345,149 @@ mod tests {
12971345
"delete_gist: destructive operation must require writer-level user integrity",
12981346
);
12991347
}
1348+
1349+
#[test]
1350+
fn apply_tool_labels_get_code_quality_finding_inherits_repo_visibility() {
1351+
let ctx = default_ctx();
1352+
let args = serde_json::json!({"owner": "octocat", "repo": "hello-world"});
1353+
let repo_id = "octocat/hello-world";
1354+
1355+
let (_, integrity, _) = super::apply_tool_labels(
1356+
"get_code_quality_finding",
1357+
&args,
1358+
repo_id,
1359+
vec![],
1360+
vec![],
1361+
String::new(),
1362+
&ctx,
1363+
);
1364+
assert_eq!(
1365+
integrity,
1366+
writer_integrity(repo_id, &ctx),
1367+
"get_code_quality_finding: expected writer-level integrity",
1368+
);
1369+
}
1370+
1371+
#[test]
1372+
fn apply_tool_labels_ui_get_labels_milestones_branches_are_repo_scoped() {
1373+
let ctx = default_ctx();
1374+
let repo_id = "octocat/hello-world";
1375+
let expected_integrity = writer_integrity(repo_id, &ctx);
1376+
1377+
for method in &["labels", "milestones", "branches"] {
1378+
let args = serde_json::json!({
1379+
"owner": "octocat",
1380+
"repo": "hello-world",
1381+
"method": method,
1382+
});
1383+
let (_, integrity, _) = super::apply_tool_labels(
1384+
"ui_get",
1385+
&args,
1386+
repo_id,
1387+
vec![],
1388+
vec![],
1389+
String::new(),
1390+
&ctx,
1391+
);
1392+
assert_eq!(
1393+
integrity, expected_integrity,
1394+
"ui_get method={method}: expected writer-level integrity",
1395+
);
1396+
}
1397+
}
1398+
1399+
#[test]
1400+
fn apply_tool_labels_ui_get_issue_types_and_fields_are_github_approved() {
1401+
let ctx = default_ctx();
1402+
let repo_id = "octocat/hello-world";
1403+
1404+
for (method, standalone) in &[("issue_types", "list_issue_types"), ("issue_fields", "list_issue_fields")] {
1405+
let args = serde_json::json!({
1406+
"owner": "octocat",
1407+
"repo": "hello-world",
1408+
"method": method,
1409+
});
1410+
let (_, integrity, _) = super::apply_tool_labels(
1411+
"ui_get",
1412+
&args,
1413+
repo_id,
1414+
vec![],
1415+
vec![],
1416+
String::new(),
1417+
&ctx,
1418+
);
1419+
// Mirror the corresponding standalone tool: ui_get issue_types/issue_fields
1420+
// should produce the same integrity as list_issue_types/list_issue_fields.
1421+
let (_, expected_integrity, _) = super::apply_tool_labels(
1422+
standalone,
1423+
&args,
1424+
repo_id,
1425+
vec![],
1426+
vec![],
1427+
String::new(),
1428+
&ctx,
1429+
);
1430+
assert_eq!(
1431+
integrity, expected_integrity,
1432+
"ui_get method={method}: expected same integrity as {standalone}",
1433+
);
1434+
}
1435+
}
1436+
1437+
#[test]
1438+
fn apply_tool_labels_ui_get_assignees_and_reviewers_are_access_sensitive() {
1439+
let ctx = default_ctx();
1440+
let repo_id = "octocat/hello-world";
1441+
let expected_integrity = reader_integrity(repo_id, &ctx);
1442+
1443+
for method in &["assignees", "reviewers"] {
1444+
let args = serde_json::json!({
1445+
"owner": "octocat",
1446+
"repo": "hello-world",
1447+
"method": method,
1448+
});
1449+
let (secrecy, integrity, _) = super::apply_tool_labels(
1450+
"ui_get",
1451+
&args,
1452+
repo_id,
1453+
vec![],
1454+
vec![],
1455+
String::new(),
1456+
&ctx,
1457+
);
1458+
let _ = secrecy; // secrecy is policy_private_scope_label (backend unavailable in tests)
1459+
assert_eq!(
1460+
integrity, expected_integrity,
1461+
"ui_get method={method}: expected reader-level integrity",
1462+
);
1463+
}
1464+
}
1465+
1466+
#[test]
1467+
fn apply_tool_labels_add_gpg_key_and_add_ssh_key_are_user_private_writes() {
1468+
let ctx = default_ctx();
1469+
let args = serde_json::json!({});
1470+
let expected_secrecy = private_user_label();
1471+
let expected_integrity = writer_integrity(scope_names::USER, &ctx);
1472+
1473+
for tool in &["add_gpg_key", "add_ssh_key"] {
1474+
let (secrecy, integrity, _) = super::apply_tool_labels(
1475+
tool,
1476+
&args,
1477+
"",
1478+
vec![],
1479+
vec![],
1480+
String::new(),
1481+
&ctx,
1482+
);
1483+
assert_eq!(
1484+
secrecy, expected_secrecy,
1485+
"{tool}: must be user-private (secrecy = private:user)",
1486+
);
1487+
assert_eq!(
1488+
integrity, expected_integrity,
1489+
"{tool}: must require writer-level user integrity",
1490+
);
1491+
}
1492+
}
13001493
}

guards/github-guard/rust-guard/src/tools.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,11 @@ pub const WRITE_OPERATIONS: &[&str] = &[
99
"actions_run_trigger",
1010
"add_comment_to_pending_review",
1111
"add_deploy_key",
12+
"add_gpg_key", // gh gpg-key add — adds a user GPG signing key
1213
"add_issue_comment",
1314
"add_project_item", // deprecated alias for projects_write (addProjectV2ItemById)
1415
"add_reply_to_pull_request_comment",
16+
"add_ssh_key", // gh ssh-key add — adds a user SSH auth/signing key
1517
"archive_repository", // gh repo archive — blocked: repo settings change unsupported
1618
"assign_copilot_to_issue",
1719
"cancel_workflow_run", // gh run cancel — cancels an in-progress workflow run
@@ -296,6 +298,8 @@ mod tests {
296298
"revert_pull_request",
297299
"add_deploy_key",
298300
"delete_deploy_key",
301+
"add_gpg_key",
302+
"add_ssh_key",
299303
] {
300304
assert!(
301305
is_write_operation(op),

0 commit comments

Comments
 (0)