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
28 changes: 22 additions & 6 deletions scripts/apply-repo-settings.sh
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#
# Companion script to compliance-audit.sh. Applies the settings defined in:
# standards/github-settings.md#repository-settings--standard-defaults
# standards/push-protection.md#required-repo-level-settings
#
# Usage:
# # Apply to a specific repo:
Expand Down Expand Up @@ -82,11 +83,12 @@ apply_labels() {

apply_settings() {
local repo="$1"
local repo_json="$2"
info "Applying standard settings to $ORG/$repo ..."

# Fetch current settings
# Extract current settings from the pre-fetched repo JSON
local current
current=$(gh api "repos/$ORG/$repo" --jq '{
current=$(echo "$repo_json" | jq '{
allow_auto_merge: .allow_auto_merge,
delete_branch_on_merge: .delete_branch_on_merge,
allow_squash_merge: .allow_squash_merge,
Expand All @@ -98,8 +100,8 @@ apply_settings() {
squash_merge_commit_message: .squash_merge_commit_message
}' 2>/dev/null || echo "{}")

if [ "$current" = "{}" ]; then
err "Could not fetch settings for $ORG/$repo — check token permissions and repo name"
if [ "$current" = "{}" ] || [ "$current" = "null" ]; then
err "Could not parse settings for $ORG/$repo"
return 1
fi

Expand Down Expand Up @@ -249,7 +251,15 @@ if [ "$1" = "--all" ]; then

failed=0
for repo in $repos; do
apply_settings "$repo" || failed=$((failed + 1))
# Fetch full repo JSON once and share across functions
repo_json=$(gh api "repos/$ORG/$repo" 2>/dev/null || echo "{}")
if [ "$repo_json" = "{}" ]; then
err "Could not fetch settings for $ORG/$repo — check token permissions and repo name"
failed=$((failed + 1))
continue
fi

apply_settings "$repo" "$repo_json" || failed=$((failed + 1))
apply_labels "$repo"
pp_apply_security_and_analysis "$repo" || failed=$((failed + 1))
apply_codeql_default_setup "$repo" || failed=$((failed + 1))
Expand All @@ -262,7 +272,13 @@ if [ "$1" = "--all" ]; then

ok "All repos processed successfully"
else
apply_settings "$1"
repo_json=$(gh api "repos/$ORG/$1" 2>/dev/null || echo "{}")
if [ "$repo_json" = "{}" ]; then
err "Could not fetch settings for $ORG/$1 — check token permissions and repo name"
exit 1
fi

apply_settings "$1" "$repo_json"
apply_labels "$1"
pp_apply_security_and_analysis "$1"
apply_codeql_default_setup "$1"
Expand Down
14 changes: 11 additions & 3 deletions scripts/apply-rulesets.sh
Original file line number Diff line number Diff line change
Expand Up @@ -135,11 +135,14 @@ detect_required_checks() {
if echo "$workflows" | grep -qx "ci.yml"; then
local ci_wf_name
ci_wf_name=$(workflow_name "ci.yml")
# Fetch ci.yml content once; used for first-job detection and secret-scan check below
local ci_content ci_decoded
ci_content=$(gh api "repos/$ORG/$repo/contents/.github/workflows/ci.yml" \
--jq '.content' 2>/dev/null || echo "")
ci_decoded=$(echo "$ci_content" | base64 -d 2>/dev/null || echo "")
# Fetch the first job name from ci.yml
local ci_job_name
ci_job_name=$(gh api "repos/$ORG/$repo/contents/.github/workflows/ci.yml" \
--jq '.content' 2>/dev/null \
| base64 -d 2>/dev/null \
ci_job_name=$(echo "$ci_decoded" \
| awk '
/^jobs:/ { in_jobs=1; found=0; next }
in_jobs && /^ [a-zA-Z0-9_-]+:/ && !found {
Expand All @@ -158,6 +161,11 @@ detect_required_checks() {
else
checks+=("$ci_job_name")
fi

# --- Secret scan (gitleaks) — included when ci.yml contains the gitleaks action ---
if echo "$ci_decoded" | grep -qE 'uses:[[:space:]]*(gitleaks/gitleaks-action|zricethezav/gitleaks-action)@'; then
checks+=("Secret scan (gitleaks)")
fi
fi

# Output as newline-separated list (guard against empty array with set -u)
Expand Down
20 changes: 17 additions & 3 deletions scripts/compliance-audit.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
# standards/ci-standards.md
# standards/dependabot-policy.md
# standards/github-settings.md
# standards/push-protection.md
#
# Outputs:
# $REPORT_DIR/findings.json — machine-readable findings
Expand Down Expand Up @@ -283,9 +284,10 @@ check_dependabot_config() {
# ---------------------------------------------------------------------------
check_repo_settings() {
local repo="$1"
local repo_json="$2"

local settings
settings=$(gh_api "repos/$ORG/$repo" --jq '{
settings=$(echo "$repo_json" | jq '{
allow_auto_merge: .allow_auto_merge,
delete_branch_on_merge: .delete_branch_on_merge,
has_wiki: .has_wiki,
Expand Down Expand Up @@ -968,6 +970,7 @@ create_umbrella_issue() {
# Each group: category_keys|display_name|remediation_script
local groups=(
"settings|Repository Settings|apply-repo-settings.sh"
"push-protection|Push Protection & Secret Scanning|apply-repo-settings.sh (security_and_analysis) + per-repo ci.yml and .gitignore"
"labels|Labels|apply_labels() in apply-repo-settings.sh"
"rulesets|Repository Rulesets|apply-rulesets.sh"
"ci-workflows|Workflows|per-repo workflow additions"
Expand Down Expand Up @@ -1156,7 +1159,7 @@ HEREDOC

HEREDOC

for category in ci-workflows action-pinning dependabot settings labels rulesets standards; do
for category in ci-workflows action-pinning dependabot settings push-protection labels rulesets standards; do
local cat_count
cat_count=$(jq --arg cat "$category" '[.[] | select(.category == $cat)] | length' "$FINDINGS_FILE")
if [ "$cat_count" -gt 0 ]; then
Expand Down Expand Up @@ -1218,10 +1221,21 @@ main() {
info "Detected ecosystems: ${ECOSYSTEMS[*]}"
fi

# Fetch full repo JSON once and share with settings/push-protection checks
local repo_json
repo_json=$(gh_api "repos/$ORG/$repo" 2>/dev/null || echo "{}")
if [ "$repo_json" = "{}" ]; then
add_finding "$repo" "settings" "repo_metadata_unavailable" "error" \
"Could not fetch repository metadata; settings and push-protection checks were skipped" \
"standards/github-settings.md#repository-settings--standard-defaults"
log_end
continue
fi

check_required_workflows "$repo"
check_action_pinning "$repo"
check_dependabot_config "$repo"
check_repo_settings "$repo"
check_repo_settings "$repo" "$repo_json"
check_labels "$repo"
check_rulesets "$repo"
check_codeowners "$repo"
Expand Down
3 changes: 2 additions & 1 deletion scripts/lib/push-protection.sh
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ pp_check_secret_scan_ci_job() {
return
fi

if ! echo "$ci_content" | grep -qiE 'gitleaks'; then
# Match actual action references, not bare mentions in comments or docs.
if ! echo "$ci_content" | grep -qE 'uses:[[:space:]]*(gitleaks/gitleaks-action|zricethezav/gitleaks-action)@'; then
add_finding "$repo" "push-protection" "secret_scan_ci_job_present" "error" \
"\`ci.yml\` does not contain a job using \`gitleaks\` — add the secret-scan job from the standard" \
"$PP_STANDARD_REF#required-ci-job"
Expand Down
7 changes: 4 additions & 3 deletions standards/github-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,9 @@ rules are deprecated — migrate existing classic rules to rulesets.

### `code-quality` — Required Checks Ruleset (All Repositories)

Every repository MUST have all five quality checks configured and required.
The specific check names and ecosystem configurations vary by repo, but the
categories are universal.
Every repository MUST have the following quality checks configured and
required. The specific check names and ecosystem configurations vary by repo,
but the categories are universal.

#### Required Check Categories

Expand All @@ -155,6 +155,7 @@ categories are universal.
| **Claude Code** | All repos | `claude` | AI code review on every PR |
| **CI Pipeline** | All repos | Repo-specific (e.g., `build-and-test`, `TypeScript`, `Go`) | Lint, format, typecheck, test |
| **Coverage** | All repos | `coverage` or embedded in CI job | Must meet repo-defined thresholds |
| **Secret Scan** | All repos | `Secret scan (gitleaks)` | Full-history gitleaks scan — see [Push Protection Standard](push-protection.md#layer-3--ci-secret-scanning-secondary-defense) |

#### Ecosystem-Specific Configuration

Expand Down
17 changes: 10 additions & 7 deletions standards/push-protection.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,17 @@ compliance audit checks these flags via `GET /repos/{owner}/{repo}`:
Apply per repo via:

```bash
gh api -X PATCH "repos/petry-projects/<repo>" \
-F security_and_analysis='{
gh api -X PATCH "repos/petry-projects/<repo>" --input - <<'JSON'
{
"security_and_analysis": {
"secret_scanning": {"status": "enabled"},
"secret_scanning_push_protection": {"status": "enabled"},
"secret_scanning_ai_detection": {"status": "enabled"},
"secret_scanning_non_provider_patterns": {"status": "enabled"},
"dependabot_security_updates": {"status": "enabled"}
}'
}
}
JSON
```
Comment thread
coderabbitai[bot] marked this conversation as resolved.

`scripts/apply-repo-settings.sh` MUST enforce these values alongside the
Expand All @@ -124,12 +127,12 @@ weekly audit. See [Application](#application-to-a-repository) below.
The org MUST configure the following custom patterns in addition to the
provider-supplied ones:

| Pattern name | Regex (illustrative) | Rationale |
|--------------|----------------------|-----------|
| Pattern name | Pattern (illustrative) | Rationale |
|--------------|------------------------|-----------|
| `petry-internal-webhook` | `https://hooks\.petry-projects\.internal/[A-Za-z0-9/_-]{20,}` | Internal webhook URLs |
| `claude-oauth-token` | `sk-ant-oat01-[A-Za-z0-9_-]{40,}` | Anthropic OAuth tokens |
| `gha-pat-scoped` | `github_pat_[A-Za-z0-9_]{82}` | Fine-grained GitHub PATs (provider pattern supplements) |
| `generic-high-entropy` | High-entropy strings assigned to `*_TOKEN`, `*_SECRET`, `*_KEY` env vars | Catches untyped long strings in YAML and `.env` files |
| `generic-high-entropy` | `(?:_TOKEN\|_SECRET\|_KEY)\s*[:=]\s*["']?[A-Za-z0-9/+=_-]{32,}` | Catches untyped long strings in YAML and `.env` files |

Custom patterns are configured at **Org settings → Code security → Secret
scanning → Custom patterns**. Each new pattern MUST be dry-run against all
Expand Down Expand Up @@ -414,7 +417,7 @@ both at once:
| `non_provider_patterns_enabled` | warning | `security_and_analysis.secret_scanning_non_provider_patterns.status == "enabled"` |
| `dependabot_security_updates_enabled` | warning | `security_and_analysis.dependabot_security_updates.status == "enabled"` |
| `open_secret_alerts` | error | `GET /repos/{owner}/{repo}/secret-scanning/alerts?state=open` returns an empty array |
| `secret_scan_ci_job_present` | error | `.github/workflows/ci.yml` contains a job using `gitleaks` |
| `secret_scan_ci_job_present` | error | `.github/workflows/ci.yml` contains a job using `gitleaks/gitleaks-action` |
| `gitignore_secrets_block` | warning | `.gitignore` contains `.env`, `*.pem`, `*.key` entries |
| `push_protection_bypasses_recent` | warning | No bypasses in the last 30 days without a documented justification |

Expand Down
Loading