Files
fleet/.github/zizmor-gate.yml
Sharon Katz 0fd5739514 Auto-create QA tickets for Dependabot PRs (#48008)
**Related issue:** Resolves #48007

# Checklist for submitter

- [x] Changes file added for user-visible changes in `changes/`
- [x] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements), JS
inline code is prevented especially for url redirects, and untrusted
data interpolated into shell scripts/commands is validated against shell
metacharacters.

## What

When Dependabot opens a code-dependency PR (Go or npm), a GitHub Actions
workflow now auto-creates a QA tracking issue assigned to
@AndreyKizimenko and places it on the `:help-qa` board in "Ready"
status. Also exempts `dependabot[bot]` from the PR template check.

### Files changed

- **`.github/workflows/dependabot-qa-ticket.yml`** (new) - The core
workflow. Triggers on `pull_request_target[opened]`, filters for
code-dependency Dependabot PRs (Go modules + npm, excluding
website/slackbot), creates a QA tracking issue, comments on the PR, and
adds the issue to the `:help-qa` project board.
- **`.github/workflows/check-pr-template.yml`** - Exempts
`dependabot[bot]` from the PR description template check (Dependabot PRs
have auto-generated descriptions).
- **`.github/zizmor-gate.yml`** - Suppresses two expected zizmor
findings: `dangerous-triggers` (uses `pull_request_target` but never
checks out PR code) and `bot-conditions` (gates on `user.login` which is
not spoofable for same-repo Dependabot PRs).
- **`changes/48007-dependabot-qa-ticket`** - Release note.

## Security review

This workflow uses `pull_request_target`, a known attack surface for
GitHub Actions. A dedicated security audit was performed covering all
standard threat vectors:

| Area | Status | Details |
|------|--------|---------|
| **Code checkout injection** | SAFE | No `actions/checkout` or `git
clone`. The workflow only calls the GitHub API via
`actions/github-script`. The inline JavaScript is embedded in the
base-branch YAML, not loaded from PR code. |
| **Script/template injection** | SAFE | PR data (`pr.title`,
`pr.head.ref`) is used in JavaScript template literals passed to the
GitHub API as strings. There is no shell interpolation, no `eval()`, no
`${{ }}` expressions in `run:` blocks. |
| **Bot identity spoofing** | SAFE | Gates on
`github.event.pull_request.user.login == 'dependabot[bot]'`, which is
server-set by GitHub and cannot be spoofed. Uses `user.login`
(immutable) rather than `github.actor` (which can differ on re-runs).
The `[bot]` suffix is reserved for GitHub App accounts. |
| **Secret exposure** | SAFE | `FLEET_GITHUB_TOKEN_PROJECTS` is passed
via `env:` block, accessed as `process.env.PROJECT_TOKEN`, and used only
as an argument to `getOctokit()`. Never logged, never interpolated into
output. |
| **Permission scope** | MINIMAL | Top-level: `contents: read`.
Job-level: `contents: read` + `issues: write`. No `pull-requests: write`
(PR comments use the Issues API). |
| **Idempotency** | SAFE | Before creating an issue, searches for an
existing open issue with the same title and `dependabot` label. Prevents
duplicates on workflow re-runs. |
| **Markdown injection** | LOW (theoretical) | `pr.title` is
interpolated into issue title/body. Dependabot controls the title format
("Bump X from Y to Z") and package names are registry-constrained. Not
exploitable without compromising Dependabot itself. |
| **Denial of service** | LOW (theoretical) | Bounded by Dependabot's
own rate limits (default 5 PRs/ecosystem/day) plus the idempotency
guard. |
| **Zizmor waivers** | JUSTIFIED | `dangerous-triggers`: no code
checkout, API-only. `bot-conditions`: uses `user.login` not
`github.actor`. Both include inline rationale comments. |
| **check-pr-template.yml** | SAFE | `AUTHOR` is passed via `env:` (not
`${{ }}` in shell), double-quoted in comparison. `user.login` is
GitHub-constrained to alphanumeric + hyphens. |

**Actions are SHA-pinned** with version comments:
`step-security/harden-runner@9af8...` (v2.19.4) and
`actions/github-script@60a0...` (v7.0.1).

## Testing

- [x] QA'd all new/changed functionality manually

Since this is a GitHub Actions workflow that triggers on Dependabot PR
opens, it cannot be fully end-to-end tested without a real Dependabot
PR. The following offline validations were performed:

### 1. YAML syntax validation
```
$ ruby -e "require 'yaml'; YAML.safe_load(File.read('.github/workflows/dependabot-qa-ticket.yml')); puts 'YAML: valid'"
YAML: valid
```

### 2. Verified hardcoded GraphQL IDs against the live GitHub API

**Project board ID** (`PVT_kwDOBDAnic4A5q11`):
```
$ gh api graphql -f query='query { node(id: "PVT_kwDOBDAnic4A5q11") { ... on ProjectV2 { title number url } } }'
-> title: ":help-qa", number: 85, url: "https://github.com/orgs/fleetdm/projects/85"
```

**Status field ID** (`PVTSSF_lADOBDAnic4A5q11zgubZ6Y`) and option ID
(`f75ad846`):
```
$ gh api graphql -f query='query { node(id: "PVTSSF_lADOBDAnic4A5q11zgubZ6Y") { ... on ProjectV2SingleSelectField { name options { id name } } } }'
-> field name: "Status", option f75ad846 = "Ready" 
```

### 3. Verified assignee and label exist
```
$ gh api users/AndreyKizimenko --jq '.login'   -> AndreyKizimenko 
$ gh label list --search dependabot             -> dependabot 
```

### 4. Branch filtering logic unit tested

Extracted the JavaScript filter logic and ran it against 8 test cases
covering single-package updates, grouped updates, and exclusions:

```
$ node -e "<test script>"
PASS | dependabot/go_modules/golang.org/x/crypto-0.24.0      | match (Go single)
PASS | dependabot-go_modules-google.golang.org-grpc-1.82.1    | match (Go grouped)
PASS | dependabot/npm_and_yarn/typescript-5.5.0               | match (npm single)
PASS | dependabot-npm_and_yarn-postcss-8.5.18                 | match (npm grouped)
PASS | dependabot/npm_and_yarn/website/next-14.0.0            | skip (website single)
PASS | dependabot-npm_and_yarn-website-next-14.0.0            | skip (website grouped)
PASS | dependabot/npm_and_yarn/fleet-slackbot/axios-1.7.0     | skip (slackbot single)
PASS | dependabot/github_actions/actions/checkout-4            | skip (GitHub Actions)
8/8 passed
```

### 5. Validated filter against real Dependabot PRs

Checked the 20 most recent Dependabot PRs (`gh pr list --author
'app/dependabot' --state all --limit 20`) to confirm real branch names
match the filter patterns. This revealed three bugs that were fixed:

1. **Go branch prefix was wrong**: Real branches use
`dependabot/go_modules/`, not `dependabot/gomod/`. Fixed.
2. **Grouped updates use dashes**: Real grouped branches like
`dependabot-go_modules-...` and `dependabot-npm_and_yarn-...` were not
matched. Added dash-variant prefix checks and exclusions.
3. **Status label mismatch**: The constant was named `AWAITING_QA` but
the option ID maps to "Ready" on the board (there is no "Awaiting QA"
status). Renamed to `READY` and updated comments.

### 6. CI checks
All CI checks pass (CodeQL, zizmor, dependency-review, build-binaries,
publish).

**Note:** The project board integration requires a
`FLEET_GITHUB_TOKEN_PROJECTS` repo secret with Organization Projects
read/write scope. Without it, the issue is still created but not added
to the board (a warning is logged).

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added an automated GitHub Actions workflow for eligible Dependabot
update pull requests.
* Creates (and deduplicates) labeled QA issues with pull request details
and checklist content, and posts the issue back to the pull request.
* Optionally links new QA issues to project tracking with an “Awaiting
QA” status when configured.

* **Chores**
  * Hardened the automation workflow’s runner and permissions.
* Updated safety-gate configuration to waive checks for the new
Dependabot QA workflow.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-07-28 13:43:52 -04:00

86 lines
4.0 KiB
YAML

# zizmor "blocking gate" configuration.
#
# This config is used by the zizmor job in .github/workflows/zizmor.yml.
#
# This file is passed explicitly via the action's `config:` input rather than being named
# .github/zizmor.yml, so a plain `zizmor .` run still reports the full backlog locally.
rules:
# The misfeature rule stays ENABLED so new workflows are still checked; we only waive the
# specific Windows signing/build workflows below. Their findings are all `shell: cmd` notices:
# zizmor cannot introspect CMD scripts, so it emits a Low-severity analysis-limitation note
# rather than a real finding, and these workflows require the Windows CMD shell. Matching is by
# filename, so keep these names unique.
misfeature:
ignore:
- build-fleetctl-msi.yml
- code-sign-windows.yml
- e2e-agent.yml
- fleet-and-orbit.yml
- generate-osqueryd-targets.yml
- goreleaser-fleet.yaml
# The template-injection rule stays ENABLED so new workflows are checked, and we waive only the
# workflows that carry the existing backlog (listed below). We audited every current finding by
# trigger and source: none are reachable by untrusted input. There are no pull_request_target /
# workflow_run / issue_comment workflows; the one issues-triggered workflow
# (auto-tag-unreleased-bugs.yml) reads the issue body via actions/github-script's context object,
# never interpolated into a shell. The remaining findings draw only from maintainer-/runner-
# controlled sources (static matrix values, workflow_dispatch inputs gated behind write access,
# push/tag refs, github.sha, and internal step outputs), so they are not externally exploitable.
# This waiver freezes the backlog: any template-injection in a NEW workflow, or any NEW finding
# in a workflow not listed here, fails the gate.
#
# Matching is by filename only (zizmor does not match on path), so keep these names unique.
# NOTE: `action.yml` matches every composite-action manifest. Today only .github/actions/r2-upload
# carries findings; a future finding in another composite action (e.g. eng-metrics) would also be
# suppressed. Revisit if more composite actions are added.
# dependabot-qa-ticket.yml uses pull_request_target for write access but never checks out
# PR code -- it only calls the GitHub API via actions/github-script, so there is no code-
# injection vector.
dangerous-triggers:
ignore:
- dependabot-qa-ticket.yml
# dependabot-qa-ticket.yml gates on github.event.pull_request.user.login (the PR author),
# not github.actor. zizmor still flags the pull_request_target bot condition; waived because
# user.login is not spoofable for same-repo Dependabot PRs.
bot-conditions:
ignore:
- dependabot-qa-ticket.yml
template-injection:
ignore:
- action.yml # .github/actions/r2-upload/action.yml (bash; env/inputs from trusted callers)
- build-fleetctl-msi.yml
- build-fleetctl-pkg.yml
- build-fleetd-base-msi.yml
- build-fleetd-base-pkg.yml
- build-fleetdm-fleetctl-check-vulnerabilities.yml
- check-bomutils-vulnerabilities.yml
- check-vulnerabilities-in-released-docker-images.yml
- check-wix-vulnerabilities.yml
- code-sign-windows.yml
- db-upgrade-test.yml
- dogfood-automated-policy-updates.yml
- dogfood-update-testing-qa-apps.yml
- e2e-agent.yml
- fleet-and-orbit.yml
- generate-desktop-targets.yml
- golangci-lint.yml
- goreleaser-fleet.yaml
- goreleaser-orbit.yaml
- goreleaser-snapshot-fleet.yaml
- ingest-maintained-apps.yml
- loadtest-infra.yml
- loadtest-osquery-perf.yml
- product-eng-handbook-summary.yml
- publish-go-module.yml
- randokiller-go.yml
- release-fleetd-base.yml
- release-fleetd-chrome-beta.yml
- release-fleetd-chrome.yml
- tag-aging-bugs.yml
- test-db-changes.yml
- test-fma-darwin-pr-only.yml
- test-fma-windows-pr-only.yml
- test-go-suite.yaml
- verify-fleetd-base.yml