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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ jobs:
run: sudo apt-get install -y bats

- name: Run tests
run: bats tests/fleet_report.bats tests/token_report.bats tests/model_pricing.bats tests/test_review_cycle.bats tests/test_ci_status.bats
run: bats tests/fleet_report.bats tests/token_report.bats tests/model_pricing.bats tests/test_review_cycle.bats tests/test_ci_status.bats tests/test_push_protection.bats
Comment thread
don-petry marked this conversation as resolved.
Comment thread
don-petry marked this conversation as resolved.

validate-agent-profiles:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ This is the `.github-private` org infrastructure repo for `petry-projects`. It c
workflow with no corresponding org template in `standards/workflows/`. It must not be removed by template
syncs. If the org template gains a token-report equivalent, remove this exception and defer to the
template instead.
- **Exception:** The `bats` test list in `lint.yml` is extended with repo-specific test files (e.g.
`tests/test_push_protection.bats`) beyond the org template baseline. When adding new test files to this
repo, add them to this list. Template syncs must not reset it to the base template list.

### Agent Profiles (`agents/*.md`)

Expand Down
22 changes: 18 additions & 4 deletions scripts/aw-standards-sync.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

set -euo pipefail

# shellcheck source=scripts/lib/push-protection.sh
source "$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/lib/push-protection.sh"

ORG="${ORG:-petry-projects}"
STANDARDS_REPO="${STANDARDS_REPO:-${ORG}/.github}"
REPORT_REPO="${REPORT_REPO:-${ORG}/.github-private}"
Expand All @@ -26,10 +29,16 @@ REQUIRED_FILES=(
".github/CODEOWNERS|standards/CODEOWNERS"
)

# Required repository security settings: "setting_label|jq_path|api_payload"
REQUIRED_SECURITY_SETTINGS=(
"secret_scanning_ai_detection|.security_and_analysis.secret_scanning_ai_detection.status|{\"security_and_analysis\":{\"secret_scanning_ai_detection\":{\"status\":\"enabled\"}}}"
)
# Required repository security settings derived from PP_REQUIRED_SA_SETTINGS in push-protection.sh.
# Format: "setting_label|jq_path|api_payload"
REQUIRED_SECURITY_SETTINGS=()
for _key in "${PP_REQUIRED_SA_SETTINGS[@]}"; do
_payload=$(jq -n --arg k "$_key" '{"security_and_analysis": {($k): {"status": "enabled"}}}')
REQUIRED_SECURITY_SETTINGS+=(
"${_key}|.security_and_analysis.${_key}.status|${_payload}"
)
done
unset _key _payload

echo "=== Standards Sync ==="
echo " Org: ${ORG}"
Expand Down Expand Up @@ -145,6 +154,11 @@ while IFS= read -r repo; do
echo " [warn] ${repo} — could not read ${setting_label}"
security_notes="${security_notes} \`${setting_label}\`:error"
else
# dependabot_security_updates requires Dependabot alerts to be enabled first.
if [ "$setting_label" = "dependabot_security_updates" ]; then
gh api -X PUT "repos/${repo}/vulnerability-alerts" --silent 2>/dev/null || \
echo " [warn] ${repo} — could not enable vulnerability alerts (prerequisite for dependabot_security_updates)"
fi
if printf '%s' "$api_payload" | \
gh api "repos/${repo}" --method PATCH --input - --silent 2>/dev/null; then
settings_applied=$((settings_applied + 1))
Expand Down
50 changes: 50 additions & 0 deletions scripts/lib/push-protection.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
Comment thread
don-petry marked this conversation as resolved.
# push-protection.sh — required security_and_analysis settings and apply helper.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
#
# Sourced by scripts that need to enforce or audit the push-protection standard:
# https://github.com/petry-projects/.github/blob/main/standards/push-protection.md
#
# PP_REQUIRED_SA_SETTINGS lists the security_and_analysis keys that must be
# "enabled" on every petry-projects repository. Adding a key here
# automatically extends both apply and audit logic that sources this file.
#
# Usage:
# source scripts/lib/push-protection.sh
# REPO=owner/repo pp_apply_security_and_analysis

set -euo pipefail

readonly -a PP_REQUIRED_SA_SETTINGS=(
secret_scanning
secret_scanning_push_protection
secret_scanning_ai_detection
secret_scanning_non_provider_patterns
dependabot_security_updates
Comment thread
don-petry marked this conversation as resolved.
Comment thread
don-petry marked this conversation as resolved.
Comment thread
don-petry marked this conversation as resolved.
)

# pp_apply_security_and_analysis
# Patches all PP_REQUIRED_SA_SETTINGS to "enabled" on $REPO.
# Honours DEV_LEAD_DRY_RUN=true (prints intent, makes no API call).
pp_apply_security_and_analysis() {
: "${REPO:?pp_apply_security_and_analysis: REPO must be set}"

if [ "${DEV_LEAD_DRY_RUN:-false}" = "true" ]; then
echo "[dry-run] would patch security_and_analysis on ${REPO}: ${PP_REQUIRED_SA_SETTINGS[*]}"
return 0
fi

# Enable Dependabot alerts — prerequisite for dependabot_security_updates.
gh api -X PUT "repos/${REPO}/vulnerability-alerts" --silent 2>/dev/null || \
echo "[warn] ${REPO} — could not enable vulnerability alerts (prerequisite for dependabot_security_updates)" >&2

local failures=0
for key in "${PP_REQUIRED_SA_SETTINGS[@]}"; do
local payload
payload=$(jq -n --arg k "$key" '{"security_and_analysis": {($k): {"status": "enabled"}}}')
if ! gh api -X PATCH "repos/${REPO}" --input - <<<"$payload"; then
echo "[warn] ${REPO} — could not enable ${key} (may require Advanced Security)" >&2
failures=$((failures + 1))
fi
Comment thread
don-petry marked this conversation as resolved.
done
return 0
}
34 changes: 34 additions & 0 deletions tests/aw/standards-sync/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,37 @@ required additions.
- Script continues processing remaining repos after the failed PATCH
- Warning message includes the setting name and a note about Advanced Security
- `settings_applied` counter does NOT increment for the failed repo

---

## Scenario 10: All PP_REQUIRED_SA_SETTINGS are enforced by the sync

**Given** a repo is missing any of the settings in `PP_REQUIRED_SA_SETTINGS`:
`secret_scanning`, `secret_scanning_push_protection`, `secret_scanning_ai_detection`,
`secret_scanning_non_provider_patterns`, `dependabot_security_updates`
**When** the monthly schedule fires
**Then**
- the workflow detects each missing setting via `REQUIRED_SECURITY_SETTINGS` (derived from `PP_REQUIRED_SA_SETTINGS` in `scripts/lib/push-protection.sh`)
- each setting is patched individually so that a failure on one does not block the others
- the summary issue notes which settings were enabled and which errored

**Assertions:**
- All five settings in `PP_REQUIRED_SA_SETTINGS` appear in `REQUIRED_SECURITY_SETTINGS`
- Adding a new key to `PP_REQUIRED_SA_SETTINGS` in the lib automatically extends the sync

---

## Scenario 11: One required security setting unavailable, others still applied

**Given** a repo has `secret_scanning_ai_detection` unavailable (no Advanced Security licence)
but the other four settings are patchable
**When** the monthly schedule fires
**Then**
- the PATCH for `secret_scanning_ai_detection` returns a 422 error
- the remaining four settings are still patched successfully in their own independent PATCH calls
- the summary issue row for the repo shows which settings were enabled and which errored

**Assertions:**
- `settings_applied` increments by 4 (the four that succeeded)
- The repo row in the summary notes `secret_scanning_ai_detection:error` alongside the four `:enabled` entries
- Script exits 0 for the run (individual setting failures are non-fatal)
146 changes: 146 additions & 0 deletions tests/test_push_protection.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
#!/usr/bin/env bats
# Unit tests for scripts/lib/push-protection.sh
#
# Run with: bats tests/test_push_protection.bats
# Install bats: https://github.com/bats-core/bats-core

setup() {
source "$(dirname "$BATS_TEST_FILENAME")/../scripts/lib/push-protection.sh"
# Capture API calls via a temp file so cross-subshell checks work.
GH_LOG="$(mktemp)"
gh() { echo "$*" >> "$GH_LOG"; }
export -f gh
export GH_LOG
unset REPO
unset DEV_LEAD_DRY_RUN
}

teardown() {
rm -f "${GH_LOG:-}"
}

# ---------------------------------------------------------------------------
# PP_REQUIRED_SA_SETTINGS — the array must include every required key
# ---------------------------------------------------------------------------

@test "PP_REQUIRED_SA_SETTINGS includes secret_scanning" {
[[ " ${PP_REQUIRED_SA_SETTINGS[*]} " == *" secret_scanning "* ]]
}

@test "PP_REQUIRED_SA_SETTINGS includes secret_scanning_push_protection" {
[[ " ${PP_REQUIRED_SA_SETTINGS[*]} " == *" secret_scanning_push_protection "* ]]
}

@test "PP_REQUIRED_SA_SETTINGS includes secret_scanning_ai_detection" {
[[ " ${PP_REQUIRED_SA_SETTINGS[*]} " == *" secret_scanning_ai_detection "* ]]
}

@test "PP_REQUIRED_SA_SETTINGS includes secret_scanning_non_provider_patterns" {
[[ " ${PP_REQUIRED_SA_SETTINGS[*]} " == *" secret_scanning_non_provider_patterns "* ]]
}

@test "PP_REQUIRED_SA_SETTINGS includes dependabot_security_updates" {
[[ " ${PP_REQUIRED_SA_SETTINGS[*]} " == *" dependabot_security_updates "* ]]
}

# ---------------------------------------------------------------------------
# pp_apply_security_and_analysis — dry-run mode
# ---------------------------------------------------------------------------

@test "dry-run skips gh api call" {
export DEV_LEAD_DRY_RUN=true
export REPO="owner/repo"
run pp_apply_security_and_analysis
[ "$status" -eq 0 ]
[ ! -s "$GH_LOG" ]
}

@test "dry-run prints what it would do" {
export DEV_LEAD_DRY_RUN=true
export REPO="owner/repo"
run pp_apply_security_and_analysis
[[ "$output" == *"dry-run"* ]]
}

# ---------------------------------------------------------------------------
# pp_apply_security_and_analysis — live mode
# ---------------------------------------------------------------------------

@test "live mode calls gh api PATCH for the repo" {
export DEV_LEAD_DRY_RUN=false
export REPO="owner/repo"
run pp_apply_security_and_analysis
[ "$status" -eq 0 ]
[ -s "$GH_LOG" ]
grep -q "api" "$GH_LOG"
grep -q "PATCH" "$GH_LOG"
grep -q "owner/repo" "$GH_LOG"
}

@test "live mode PUTs vulnerability-alerts as prerequisite before PATCH loop" {
export DEV_LEAD_DRY_RUN=false
export REPO="owner/repo"
run pp_apply_security_and_analysis
[ "$status" -eq 0 ]
grep -q "vulnerability-alerts" "$GH_LOG"
}

@test "live mode errors when REPO is unset" {
export DEV_LEAD_DRY_RUN=false
unset REPO
run pp_apply_security_and_analysis
[ "$status" -ne 0 ]
}

@test "live mode makes one PATCH call per required setting" {
export DEV_LEAD_DRY_RUN=false
export REPO="owner/repo"
gh() {
local stdin_body=""
[ ! -t 0 ] && stdin_body=$(cat)
printf '%s BODY=%s\n' "$*" "$stdin_body" >> "$GH_LOG"
}
export -f gh
run pp_apply_security_and_analysis
[ "$status" -eq 0 ]
local patch_count
patch_count=$(grep -c "PATCH" "$GH_LOG")
[ "$patch_count" -eq "${#PP_REQUIRED_SA_SETTINGS[@]}" ]
}

@test "live mode continues applying remaining settings after one PATCH failure" {
export DEV_LEAD_DRY_RUN=false
export REPO="owner/repo"
export FAIL_KEY="${PP_REQUIRED_SA_SETTINGS[0]}"
gh() {
local stdin_body=""
[ ! -t 0 ] && stdin_body=$(cat)
printf '%s BODY=%s\n' "$*" "$stdin_body" >> "$GH_LOG"
echo "$stdin_body" | grep -q "\"${FAIL_KEY}\"" && return 1
return 0
}
export -f gh
run pp_apply_security_and_analysis
# All settings should be attempted even though the first one fails
local patch_count
patch_count=$(grep -c "PATCH" "$GH_LOG")
[ "$patch_count" -eq "${#PP_REQUIRED_SA_SETTINGS[@]}" ]
}

@test "live mode returns 0 when some PATCH calls fail (partial failure is non-fatal)" {
export DEV_LEAD_DRY_RUN=false
export REPO="owner/repo"
export FAIL_KEY="${PP_REQUIRED_SA_SETTINGS[0]}"
gh() {
local stdin_body=""
[ ! -t 0 ] && stdin_body=$(cat)
printf '%s BODY=%s\n' "$*" "$stdin_body" >> "$GH_LOG"
echo "$stdin_body" | grep -q "\"${FAIL_KEY}\"" && return 1
return 0
}
export -f gh
run pp_apply_security_and_analysis
# A 422 for one unavailable setting (e.g. secret_scanning_ai_detection without GHAS)
# must not abort callers running under set -euo pipefail; exit code must be 0.
[ "$status" -eq 0 ]
}
Loading