-
Notifications
You must be signed in to change notification settings - Fork 59
feat: onboard FullSend JIRA integration #324
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| --- | ||
| # fullsend Jira poll | ||
| # | ||
| # Scheduled workflow that polls Jira for actionable issues/comments and | ||
| # dispatches the matching fullsend agent workflow for each one. | ||
| # | ||
| # Security model: | ||
| # - Runs only against this repo's trusted default-branch code (no PR | ||
| # checkout), so it is not exposed to "pwn request" style attacks. | ||
| # - The fullsend binary is pinned to a specific release and checksum-verified | ||
| # before it is given access to the Jira credentials. | ||
| # - Every Jira-derived value is validated against a strict allowlist before | ||
| # it flows into a gh CLI argument, URL, or workflow command, blocking GHA | ||
| # command injection from attacker-controlled issue content. | ||
| # | ||
| # Dispatch: agent workflows opt in by carrying a "# fullsend-stage: <stage>" | ||
| # marker line. No such consumer workflows exist in this repo yet, so records | ||
| # are logged and skipped until they land (tracked upstream in fullsend #2264). | ||
| name: fullsend jira poll | ||
|
|
||
| permissions: | ||
| actions: write | ||
| contents: read | ||
|
|
||
| on: | ||
| schedule: | ||
| - cron: "*/5 * * * *" | ||
| workflow_dispatch: {} | ||
|
|
||
| jobs: | ||
| poll: | ||
| runs-on: ubuntu-24.04 | ||
| concurrency: | ||
| group: fullsend-jira-poll | ||
| cancel-in-progress: false | ||
| steps: | ||
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | ||
| with: | ||
| persist-credentials: false | ||
|
|
||
| - name: Install fullsend | ||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| FULLSEND_VERSION: v0.36.0 | ||
| run: | | ||
| set -euo pipefail | ||
| asset="fullsend_${FULLSEND_VERSION#v}_linux_amd64.tar.gz" | ||
| gh release download "$FULLSEND_VERSION" --repo fullsend-ai/fullsend \ | ||
| -p "$asset" -p checksums.txt | ||
| sha256sum --ignore-missing -c checksums.txt | ||
| tar xzf "$asset" fullsend | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] supply-chain The fullsend binary checksum verification downloads checksums.txt from the same GitHub release as the binary. This guards against download corruption but not upstream compromise. Consistent with the existing trust model (the repo already trusts fullsend-ai/fullsend via SHA-pinned reusable workflow). Suggested fix: Consider pinning a known-good SHA256 hash directly in the workflow rather than relying on the release-hosted checksums.txt. |
||
| sudo mv fullsend /usr/local/bin/ | ||
|
samanthajayasinghe marked this conversation as resolved.
samanthajayasinghe marked this conversation as resolved.
samanthajayasinghe marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] Supply chain / unpinned dependency The Suggested fix: Pin the download to a specific release tag and verify the artifact against a known checksum or cosign signature. |
||
| rm -f "$asset" checksums.txt | ||
|
|
||
| - name: Poll Jira | ||
| env: | ||
| JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] secret-exposure JIRA_TOKEN and JIRA_USER_EMAIL secrets are exposed to the Poll Jira step which runs the unpinned fullsend binary. If the binary is compromised, these secrets are directly accessible. Suggested fix: Pin the fullsend binary version and verify its integrity. |
||
| JIRA_USER_EMAIL: ${{ secrets.JIRA_USER_EMAIL }} | ||
| JIRA_BASE_URL: ${{ vars.JIRA_BASE_URL }} | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] fail-open JIRA_BASE_URL from vars.JIRA_BASE_URL is not validated. If empty or malformed, malformed URLs propagate into dispatched workflows. Suggested fix: Add early validation that JIRA_BASE_URL is non-empty. |
||
| run: | | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] error-handling-idiom The Poll Jira step omits set -euo pipefail, unlike the Dispatch step and existing fullsend.yaml convention. Suggested fix: Add set -euo pipefail at the beginning of the Poll Jira step. |
||
| set -euo pipefail | ||
| if [[ -z "${JIRA_BASE_URL:-}" ]]; then | ||
| echo "::error::JIRA_BASE_URL is not configured" | ||
| exit 1 | ||
| fi | ||
| fullsend poll \ | ||
| --input-driver jira-poll \ | ||
| --jira-url "${JIRA_BASE_URL}" \ | ||
| --jira-project ROSAENG \ | ||
| --jql 'project = ROSAENG AND component = ocm-agent-operator AND issuetype in (Bug, Story, Feature) AND issuetype not in (Vulnerability, Weakness) AND (labels is EMPTY OR labels not in (security, cve, embargo)) AND status not in (Closed, Done, "Won'\''t Do")' \ | ||
| --target-repo "${{ github.repository }}" \ | ||
| --output dispatches.json \ | ||
| --fullsend-dir .fullsend | ||
|
|
||
| - name: Dispatch agent workflows | ||
|
samanthajayasinghe marked this conversation as resolved.
|
||
| env: | ||
| GH_TOKEN: ${{ github.token }} | ||
| JIRA_BASE_URL: ${{ vars.JIRA_BASE_URL }} | ||
| run: | | ||
| set -euo pipefail | ||
|
|
||
| if [[ -z "${JIRA_BASE_URL:-}" ]]; then | ||
| echo "::error::JIRA_BASE_URL is not configured" | ||
| exit 1 | ||
| fi | ||
|
|
||
| if ! jq -e 'length > 0' dispatches.json > /dev/null 2>&1; then | ||
| echo "No dispatches to process." | ||
| exit 0 | ||
| fi | ||
|
|
||
| dispatched=0 | ||
| count=$(jq 'length' dispatches.json) | ||
|
|
||
| for i in $(seq 0 $((count - 1))); do | ||
| record=$(jq -c ".[$i]" dispatches.json) | ||
| stage=$(echo "$record" | jq -r '.stage') | ||
| resource_key=$(echo "$record" | jq -r '.resource_key') | ||
| event_type=$(echo "$record" | jq -r '.event_type') | ||
| issue_id=$(echo "$record" | jq -r '(.iid // 0) | tonumber') | ||
| issue_key="${resource_key#issue-}" | ||
|
|
||
| # Validate every Jira-derived value against a strict allowlist before | ||
| # it reaches a gh CLI arg, URL, or workflow command. This fails | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] edge-case When a dispatch record has a null or missing iid field, the expression (.iid // 0) | tonumber silently falls back to 0, producing issue.number = 0 in the event payload. While issue_key is the primary routing identifier and is validated, passing an invalid numeric ID to downstream consumers may cause confusion. Suggested fix: Add a validation check that skips the record (with a warning) if issue_id is 0 after coercion. |
||
| # closed on anything unexpected and blocks GHA command injection | ||
| # (embedded "::" or encoded newlines) from attacker-controlled data. | ||
| if [[ ! "$stage" =~ ^[a-z][a-z0-9-]*$ ]]; then | ||
| echo "::warning::Skipping record ${i}: invalid stage" | ||
| continue | ||
| fi | ||
| if [[ ! "$event_type" =~ ^[a-z][a-z0-9_-]*$ ]]; then | ||
| echo "::warning::Skipping record ${i}: invalid event_type" | ||
| continue | ||
| fi | ||
| if [[ ! "$issue_key" =~ ^[A-Z][A-Z0-9]*-[0-9]+$ ]]; then | ||
| echo "::warning::Skipping record ${i}: invalid issue key" | ||
| continue | ||
| fi | ||
|
|
||
| issue_url="${JIRA_BASE_URL%/}/browse/${issue_key}" | ||
| event_payload=$(jq -nc \ | ||
| --argjson number "$issue_id" \ | ||
| --arg url "$issue_url" \ | ||
| '{issue: {number: $number, html_url: $url}}') | ||
|
|
||
| # Find the checked-in workflow that handles this stage, matched by a | ||
| # "# fullsend-stage: <stage>" marker line at any indentation. | ||
| workflow_name="" | ||
| for wf in .github/workflows/*.yml .github/workflows/*.yaml; do | ||
| [[ -f "$wf" ]] || continue | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] logic-error grep -qF performs a fixed-string substring match on the fullsend-stage marker. If two workflow files contain stage markers with a prefix relationship (e.g., plan and plan-review), the shorter stage name could incorrectly match the longer one. Currently no consumer workflows exist, making this theoretical. Suggested fix: Use a regex with a word boundary or end-of-line anchor for exact matching. |
||
| if grep -qF "# fullsend-stage: ${stage}" "$wf"; then | ||
| workflow_name=$(basename "$wf") | ||
| break | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] runtime mechanism failure The dispatch loop searches for workflow files containing a comment matching exactly '# fullsend-stage: ${STAGE}' (grep -qxF), but no existing workflow file in the repository contains any 'fullsend-stage:' comment. This means the dispatch loop will always hit the 'No workflow found' warning and skip every record, making the poll-and-dispatch pipeline a no-op until stage-specific workflow files are added. Suggested fix: Either add workflow files with '# fullsend-stage: ' comments for each role defined in .fullsend/config.yaml, or document that this is scaffolding that depends on future stage-specific workflow files. |
||
| fi | ||
| done | ||
| if [[ -z "$workflow_name" ]]; then | ||
| echo "::warning::No workflow found for stage ${stage}, skipping ${issue_key}" | ||
| continue | ||
|
samanthajayasinghe marked this conversation as resolved.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] injection GHA workflow command injection via unsanitized Jira-derived values. STAGE and RESOURCE_KEY are interpolated into ::warning:: (line 83) and echo (line 86) commands without sanitization. An attacker with Jira write access could inject ::add-mask:: or ::error:: sequences via crafted field values. Suggested fix: Sanitize STAGE, RESOURCE_KEY, and ISSUE_KEY by stripping ::, %0A, %0D, and control characters, or write messages to $GITHUB_STEP_SUMMARY instead. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [medium] injection GHA workflow command injection via unsanitized RESOURCE_KEY. RESOURCE_KEY is extracted from Jira-sourced data and interpolated directly into a ::warning:: workflow command without sanitization. If a Jira resource_key contains %0A/%0D sequences or :: delimiters, an attacker with write access to the ROSAENG Jira project could inject workflow commands. Suggested fix: Sanitize RESOURCE_KEY before interpolating into workflow commands by stripping or encoding :: sequences, %0A/%0D, and control characters. |
||
| fi | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] GHA workflow command injection The Suggested fix: Sanitize STAGE and RESOURCE_KEY before interpolating into workflow commands by stripping newlines and percent-encoded control characters. |
||
|
|
||
| echo "Dispatching ${workflow_name} for ${issue_key} (${stage})" | ||
| # Don't let a single transient dispatch failure abort the rest. | ||
| if gh workflow run "$workflow_name" \ | ||
| -f event_type="$event_type" \ | ||
| -f source_repo="${{ github.repository }}" \ | ||
|
samanthajayasinghe marked this conversation as resolved.
|
||
| -f event_payload="$event_payload"; then | ||
| dispatched=$((dispatched + 1)) | ||
| else | ||
| echo "::warning::Failed to dispatch ${workflow_name} for ${issue_key}" | ||
| fi | ||
| done | ||
|
|
||
| echo "::notice::Dispatched ${dispatched} agent workflow(s)" | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[low] shell-idiom-consistency
The Install fullsend step omits set -euo pipefail while the Dispatch agent workflows step includes it. The pipe (gh release download ... | tar xz) could silently swallow download failures without pipefail.
Suggested fix: Add set -euo pipefail at the top of the Install fullsend step's run block.