Skip to content
Merged
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
153 changes: 153 additions & 0 deletions .github/workflows/fullsend-poll-jira.yaml
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: |

Copy link
Copy Markdown

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.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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/
Comment thread
samanthajayasinghe marked this conversation as resolved.
Comment thread
samanthajayasinghe marked this conversation as resolved.
Comment thread
samanthajayasinghe marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[medium] Supply chain / unpinned dependency

The fullsend binary is downloaded from fullsend-ai/fullsend GitHub releases using a glob pattern with no version pin, tag reference, or checksum verification. Any new release would be automatically pulled and executed with the workflow's permissions (actions: write, contents: read, plus github.token access).

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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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: |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Comment thread
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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
Comment thread
samanthajayasinghe marked this conversation as resolved.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] GHA workflow command injection

The ::warning:: workflow command interpolates ${STAGE} and ${RESOURCE_KEY}, both derived from Jira data. An attacker influencing Jira fields could inject workflow commands, though practical impact is limited since dangerous commands like ::set-env:: are disabled by default since 2020.

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 }}" \
Comment thread
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)"
Loading