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
41 changes: 19 additions & 22 deletions crates/squabble-cli/src/fetch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,10 @@ fn contexts_or_no_gate(
/// refusal means the repository's own Actions posture, not the workflow's
/// content, is the first thing to check; matching is by exact context name,
/// the same key [`build_gate`] uses.
fn startup_failures_from_rollup(required_contexts: &[String], rollup: &[RollupEntry]) -> Vec<String> {
fn startup_failures_from_rollup(
required_contexts: &[String],
rollup: &[RollupEntry],
) -> Vec<String> {
required_contexts
.iter()
.filter(|req| {
Expand Down Expand Up @@ -400,8 +403,9 @@ fn parse_policy(perms_json: &str, selected_json: Option<&str>) -> Result<Actions
.map_err(|e| format!("could not parse actions/permissions response: {e}"))?;
let allowed_actions = perms.allowed_actions.unwrap_or_default();
let (github_owned_allowed, patterns_allowed) = if allowed_actions == "selected" {
let sel_json = selected_json
.ok_or_else(|| "allowed_actions=selected but no selected-actions payload".to_string())?;
let sel_json = selected_json.ok_or_else(|| {
"allowed_actions=selected but no selected-actions payload".to_string()
})?;
let sel: SelectedActionsResponse = serde_json::from_str(sel_json)
.map_err(|e| format!("could not parse selected-actions response: {e}"))?;
(sel.github_owned_allowed, sel.patterns_allowed)
Expand Down Expand Up @@ -584,16 +588,6 @@ pub fn run(slug: &str, pr: &str) -> Result<Gate, FetchError> {
run_bundle(slug, pr).map(|b| b.gate)
}

/// As [`run`], but also returns the checks that concluded **success**, with the
/// job id needed to inspect their steps.
///
/// The green set is what [`squabble_core::polarity`] classifies. `fight` only
/// ever looks at reds, so a gate that could not run reports green and is never
/// inspected — that is the whole fake-green class.
pub fn run_with_greens(slug: &str, pr: &str) -> Result<(Gate, Vec<GreenCheck>), FetchError> {
run_bundle(slug, pr).map(|b| (b.gate, b.greens))
}

/// The full live fetch: gate, inspectable greens, and the Actions-policy
/// why-probe inputs (issue #15) for any required context that refused to
/// start. `fight` consumes this; the narrower entry points project from it.
Expand Down Expand Up @@ -630,8 +624,7 @@ pub fn run_bundle(slug: &str, pr: &str) -> Result<FetchBundle, FetchError> {

let protection = probe_classic_protection(slug, &pr_view.base_ref_name)?;
let required_contexts = required_contexts_from_apis(&rules_json, &protection)?;
let required_contexts =
contexts_or_no_gate(required_contexts, slug, &pr_view.base_ref_name)?;
let required_contexts = contexts_or_no_gate(required_contexts, slug, &pr_view.base_ref_name)?;

let startup_failed =
startup_failures_from_rollup(&required_contexts, &pr_view.status_check_rollup);
Expand Down Expand Up @@ -784,9 +777,10 @@ mod tests {
// exits 0 on, never NoGate/3) AND as the exact context list, because
// asserting merely "not 3" would not close the defect.
let gated = contexts_or_no_gate(
required_contexts_from_apis("[]", &ProtectionProbe::Protected(
CLASSIC_TWO_CONTEXTS.to_string(),
))
required_contexts_from_apis(
"[]",
&ProtectionProbe::Protected(CLASSIC_TWO_CONTEXTS.to_string()),
)
.expect("classic protection must parse"),
"o/r",
"main",
Expand Down Expand Up @@ -885,15 +879,18 @@ mod tests {
#[test]
fn classic_checks_shape_is_read_without_contexts() {
// Some payloads carry only the newer `checks` objects.
let json = r#"{"required_status_checks": {"strict": false, "checks": [{"context": "gate"}]}}"#;
let json =
r#"{"required_status_checks": {"strict": false, "checks": [{"context": "gate"}]}}"#;
assert_eq!(
parse_classic_contexts(json).expect("parse"),
vec!["gate".to_string()]
);
// And protection with no status-check requirement at all yields none.
assert!(parse_classic_contexts(r#"{"enforce_admins": {"enabled": true}}"#)
.expect("parse")
.is_empty());
assert!(
parse_classic_contexts(r#"{"enforce_admins": {"enabled": true}}"#)
.expect("parse")
.is_empty()
);
}

#[test]
Expand Down
5 changes: 4 additions & 1 deletion crates/squabble-core/src/moves.rs
Original file line number Diff line number Diff line change
Expand Up @@ -640,6 +640,9 @@ mod tests {
}
// The kebab-case wire names the JSON consumers see.
let json = serde_json::to_string(&Move::SetActionsAllowedAll).unwrap();
assert!(json.contains("\"kind\":\"set-actions-allowed-all\""), "{json}");
assert!(
json.contains("\"kind\":\"set-actions-allowed-all\""),
"{json}"
);
}
}
15 changes: 7 additions & 8 deletions crates/squabble-fight/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,10 +700,7 @@ mod policy_fixture_tests {
fn the_offline_fixture_diagnoses_to_the_new_moves() {
let gate = fixture_gate();
assert_eq!(gate.checks.len(), 2, "fixture drifted — keep facts in step");
assert!(gate
.checks
.iter()
.all(|c| c.run == CheckRun::Failed));
assert!(gate.checks.iter().all(|c| c.run == CheckRun::Failed));

// The live-fetch inputs the fixture cannot carry: which contexts the
// rollup showed STARTUP_FAILURE for, and the posture the why-probe
Expand Down Expand Up @@ -792,10 +789,12 @@ mod policy_fixture_tests {
.escalations
.iter()
.any(|e| e.group == ExpertGroup::Security));
assert!(!report
.moves_attempted
.iter()
.any(|m| matches!(m, Move::SetActionsAllowedAll | Move::PinWorkflowActions { .. } | Move::ReconcileActionsPolicy { .. })));
assert!(!report.moves_attempted.iter().any(|m| matches!(
m,
Move::SetActionsAllowedAll
| Move::PinWorkflowActions { .. }
| Move::ReconcileActionsPolicy { .. }
)));
}

#[test]
Expand Down
16 changes: 12 additions & 4 deletions crates/squabble-fight/src/workflows.rs
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,7 @@
let scalar = trimmed
.strip_prefix("- run:")
.or_else(|| trimmed.strip_prefix("run:"))
.unwrap()

Check failure on line 498 in crates/squabble-fight/src/workflows.rs

View workflow job for this annotation

GitHub Actions / Hypatia neurosymbolic scan

[hypatia] unwrap() without prior check -- DoS via panic (1 occurrences, CWE-754, line 498)
.trim_start();
if scalar.starts_with('|') || scalar.starts_with('>') {
state = BlockState::Run {
Expand Down Expand Up @@ -625,7 +625,7 @@

/// The raw `uses:` target of a trimmed workflow line, with any trailing
/// comment cut and quotes removed. `None` for non-`uses:` lines.
pub(crate) fn uses_target<'a>(t: &'a str) -> Option<&'a str> {
pub(crate) fn uses_target(t: &str) -> Option<&str> {
let rest = t.strip_prefix("uses:")?.trim();
let token = rest.split_whitespace().next()?;
Some(token.trim_matches(['\'', '"']))
Expand Down Expand Up @@ -1068,7 +1068,10 @@
vec!["hyperpolymath/standards@8f2ee50841e216cd8c192eeb68953118190f105c".to_string()]
);
assert!(w.tag_pinned_uses.is_empty());
assert_eq!(w.reusable_repos, vec!["hyperpolymath/standards".to_string()]);
assert_eq!(
w.reusable_repos,
vec!["hyperpolymath/standards".to_string()]
);
}

#[test]
Expand All @@ -1083,7 +1086,10 @@
"r-lib/*".into(),
],
};
assert!(p.covers("actions/checkout"), "github-owned under github_owned_allowed");
assert!(
p.covers("actions/checkout"),
"github-owned under github_owned_allowed"
);
assert!(p.covers("github/codeql-action"));
assert!(p.covers("hyperpolymath/standards"), "owner-wide");
assert!(p.covers("oven-sh/setup-bun"), "exact owner/repo");
Expand All @@ -1101,7 +1107,9 @@
allowed_actions: "all".into(),
..p
};
assert!(all.first_uncovered(&["step-security/harden-runner@v2".to_string()]).is_none());
assert!(all
.first_uncovered(&["step-security/harden-runner@v2".to_string()])
.is_none());
}

#[test]
Expand Down
Loading