Skip to content
Open
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
240 changes: 240 additions & 0 deletions scripts/lib/post-failure-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env bash
# post-failure-report.sh — Categorized, sanitized failure comments for post-scripts.
#
# Source from post-code.sh / post-fix.sh:
# source "${SCRIPT_DIR}/lib/post-failure-report.sh"
#
# Set POST_FAILURE_CATEGORY / POST_FAILURE_DETAIL before exit, or call post_fail.

# shellcheck shell=bash

[[ -n "${POST_FAILURE_REPORT_SH_LOADED:-}" ]] && return 0
POST_FAILURE_REPORT_SH_LOADED=1

POST_FAILURE_CATEGORY="${POST_FAILURE_CATEGORY:-}"
POST_FAILURE_DETAIL="${POST_FAILURE_DETAIL:-}"
POST_FAILURE_REPORTED=false
POST_FAILURE_SECRET_SCAN_MESSAGE="Secret scan blocked the push. See workflow logs for details."

# Maximum lines of sanitized detail to include in issue/PR comments.
POST_FAILURE_DETAIL_MAX_LINES="${POST_FAILURE_DETAIL_MAX_LINES:-30}"

_redact_multiline_pem() {
awk '
/-----BEGIN [A-Z ]*PRIVATE KEY-----/ {
print "[REDACTED PRIVATE KEY]"
in_pem = 1
next
}
/-----END [A-Z ]*PRIVATE KEY-----/ {
in_pem = 0
next
}
in_pem { next }
{ print }
'
}

# Strip tokens and truncate noisy command output before posting publicly.
sanitize_failure_detail() {
local detail="$1"
local max_lines="${2:-${POST_FAILURE_DETAIL_MAX_LINES}}"

detail="$(printf '%s\n' "${detail}" \
| sed -E \
-e 's/gh[pousr]_[A-Za-z0-9_]{20,}/[REDACTED]/g' \
-e 's/github_pat_[A-Za-z0-9_]+/[REDACTED]/g' \
-e 's/x-access-token:[^@[:space:]]+/x-access-token:[REDACTED]/g' \
-e 's/(Bearer|token)[[:space:]]+[A-Za-z0-9._-]+/\1 [REDACTED]/gi' \
| _redact_multiline_pem)"

if [ "${max_lines}" -gt 0 ]; then
detail="$(printf '%s\n' "${detail}" | tail -n "${max_lines}")"
fi

printf '%s' "${detail}"
}

set_post_failure() {
POST_FAILURE_CATEGORY="$1"
POST_FAILURE_DETAIL="$2"
}

categorize_push_failure() {
local push_output="$1"

if echo "${push_output}" | grep -qiE \
'workflow.*without.*workflows?[[:space:]]+permission|refusing to allow.*GitHub App.*workflow'; then
echo "push-workflow-permission"
return 0
fi

if echo "${push_output}" | grep -qiE \
'non-fast-forward|rejected|fetch first|protected branch|GH006|permission denied'; then
echo "push-rejected"
return 0
fi

echo "push-rejected"
}

post_failure_category_label() {
case "$1" in
secret-scan) echo "Secret scan blocked" ;;
pre-commit-blocked) echo "Pre-commit blocked" ;;
signed-off-by) echo "Signed-off-by rejected" ;;
push-workflow-permission) echo "Push rejected — workflows permission" ;;
push-rejected) echo "Push rejected" ;;
pr-creation-failed) echo "PR creation failed" ;;
branch-validation) echo "Branch validation failed" ;;
setup-error) echo "Setup error" ;;
process-output-failed) echo "Structured output processing failed" ;;
*) echo "Post-script failed" ;;
esac
}

post_failure_environmental_note() {
case "$1" in
push-workflow-permission)
cat <<'EOF'
> **Environmental limitation:** the GitHub App lacks `workflows` write permission on this repository. The agent's patch is not necessarily wrong — update repo or app permissions (or avoid `.github/workflows/` changes) and retry.
EOF
;;
*)
printf ''
;;
esac
}

post_failure_workflow_run_url() {
local repo_full_name="$1"
local run_repo="${GITHUB_REPOSITORY:-${repo_full_name}}"
printf '%s/%s/actions/runs/%s' \
"${GITHUB_SERVER_URL:-https://github.com}" \
"${run_repo}" \
"${GITHUB_RUN_ID:-unknown}"
}

build_post_failure_comment() {
local agent_kind="$1" # code | fix
local exit_code="$2"
local category="$3"
local detail="$4"
local repo_full_name="$5"
local retry_command="$6"

local label env_note sanitized_detail run_url detail_block

label="$(post_failure_category_label "${category}")"
env_note="$(post_failure_environmental_note "${category}")"
run_url="$(post_failure_workflow_run_url "${repo_full_name}")"

if [ "${category}" = "secret-scan" ]; then
sanitized_detail="${POST_FAILURE_SECRET_SCAN_MESSAGE}"
else
sanitized_detail="$(sanitize_failure_detail "${detail}")"
fi

if [ -n "${sanitized_detail}" ]; then
detail_block="$(cat <<EOF

**Details:**
\`\`\`
${sanitized_detail}
\`\`\`
EOF
Comment on lines +138 to +145

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Markdown fence injection 🐞 Bug ≡ Correctness

build_post_failure_comment embeds sanitized failure output inside a triple-backtick fenced block
without escaping embedded ``` sequences, so certain command outputs can prematurely terminate the
fence and corrupt the rendered issue/PR comment. This is reachable because post-code.sh/post-fix.sh
pass raw command output (push/pre-commit/etc.) into the failure detail that becomes the comment
body.
Agent Prompt
### Issue description
`build_post_failure_comment()` builds a Markdown comment that wraps `${sanitized_detail}` in a triple-backtick fenced code block. If `${sanitized_detail}` contains ``` (or an unmatched fence-like sequence), the fence can be closed early and the rest of the comment renders incorrectly.

### Issue Context
Although the content is token-redacted, it is still untrusted command output (e.g., git push / pre-commit output) and may contain arbitrary text.

### Fix Focus Areas
- scripts/lib/post-failure-report.sh[118-166]

### Suggested fix
Update `build_post_failure_comment()` to avoid fenced blocks entirely by rendering details as an indented code block:
- Build `detail_block` by prefixing every line of `${sanitized_detail}` (including empty lines) with 4 spaces, e.g. via `sed 's/^/    /'`.
- Keep the existing token/PEM redaction and line truncation.

This removes the possibility of fence-termination by embedded backticks and preserves readability in GitHub-flavored Markdown.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

)"
else
detail_block=""
fi

if [ -n "${env_note}" ]; then
env_note="${env_note}

"
fi

cat <<EOF
⚠️ **Post-${agent_kind} script failed** — ${label} (exit code ${exit_code})

The ${agent_kind} agent completed, but the post-${agent_kind} script failed before finishing.

${env_note}**Workflow run:** ${run_url}
${detail_block}
Please check the workflow logs for full details and retry with \`${retry_command}\` if appropriate.
EOF
}

_post_failure_ensure_token() {
if [ -z "${GH_TOKEN:-}" ]; then
export GH_TOKEN="${PUSH_TOKEN:-}"
fi
}

report_post_failure_to_issue() {
local exit_code="${1:-$?}"

if [ "${POST_FAILURE_REPORTED}" = "true" ]; then
return 0
fi
POST_FAILURE_REPORTED=true

_post_failure_ensure_token

local category="${POST_FAILURE_CATEGORY:-post-script-error}"
local detail="${POST_FAILURE_DETAIL:-Post-code script failed before push or PR creation completed.}"
local body
# ISSUE_NUMBER and REPO_FULL_NAME are required by post-code.sh before sourcing.
# shellcheck disable=SC2153
body="$(build_post_failure_comment \
"code" "${exit_code}" "${category}" "${detail}" \
"${REPO_FULL_NAME}" "/fs-code")"

echo "::warning::Posting failure comment to issue #${ISSUE_NUMBER}..."
gh issue comment "${ISSUE_NUMBER}" \
--repo "${REPO_FULL_NAME}" \
--body "${body}" 2>/dev/null || \
echo "::warning::Failed to post error comment to issue #${ISSUE_NUMBER}"
}

report_post_failure_to_pr() {
local exit_code="${1:-$?}"

if [ "${POST_FAILURE_REPORTED}" = "true" ]; then
return 0
fi
POST_FAILURE_REPORTED=true

_post_failure_ensure_token

local category="${POST_FAILURE_CATEGORY:-post-script-error}"
local detail="${POST_FAILURE_DETAIL:-Post-fix script failed before push or PR update completed.}"
local body
# PR_NUMBER and REPO_FULL_NAME are required by post-fix.sh before sourcing.
# shellcheck disable=SC2153
body="$(build_post_failure_comment \
"fix" "${exit_code}" "${category}" "${detail}" \
"${REPO_FULL_NAME}" "/fs-fix")"

echo "::warning::Posting failure comment to PR #${PR_NUMBER}..."
gh pr comment "${PR_NUMBER}" \
--repo "${REPO_FULL_NAME}" \
--body "${body}" 2>/dev/null || \
echo "::warning::Failed to post error comment to PR #${PR_NUMBER}"
Comment on lines +193 to +223

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. issue_number in ::warning unsanitized 📜 Skill insight ⛨ Security

The new failure-reporting library emits GitHub Actions workflow commands (::warning::...) with
interpolated values that are not sanitized. Unsanitized interpolation can allow workflow-command
injection via ::, encoded newlines (%0A/%0D), or control characters.
Agent Prompt
## Issue description
`scripts/lib/post-failure-report.sh` prints GitHub Actions workflow commands (e.g., `::warning::...`) that include interpolated variables (e.g., `ISSUE_NUMBER`, `PR_NUMBER`) without sanitizing each variable for workflow-command injection vectors (`::`, `%0A/%0D`, ANSI/control chars).

## Issue Context
Compliance requires that **every interpolated value** in a GitHub Actions workflow command be individually sanitized, not just some values or only "likely safe" values.

## Fix Focus Areas
- scripts/lib/post-failure-report.sh[193-223]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}

post_fail_to_issue() {
local category="$1"
local detail="${2:-}"
set_post_failure "${category}" "${detail}"
report_post_failure_to_issue 1
exit 1
}

post_fail_to_pr() {
local category="$1"
local detail="${2:-}"
set_post_failure "${category}" "${detail}"
report_post_failure_to_pr 1
exit 1
}
Loading
Loading