From eb2ed5e41adc7b44d2db9790a8b611f530545674 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 14:23:23 +0000 Subject: [PATCH] ci: submit Android release runtime dependencies to the dependency graph GitHub's dependency graph sees Pub packages from pubspec.lock but not the Maven artifacts behind the Flutter plugins' Android code, so Dependabot alerts never covered the native libraries apps built with the SDK pull in. - Dependency Submission workflow: resolve the playground app's releaseRuntimeClasspath with the Gradle dependency-submission action, fail instead of submitting an empty, partial or mis-attributed snapshot, keep the snapshot as a per-commit artifact, and submit it on the default branch. Runs on main pushes, weekly and on demand (dry run off the default branch). - Dependabot gradle entry and a Dependency Review check for Pub changes. - docs/DEPENDENCY_SECURITY.md: inventory, scope, how findings are handled. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01TQhEETSsYBHyfowFNUmga6 --- .github/dependabot.yml | 9 ++ .github/workflows/dependency-review.yml | 44 +++++++ .github/workflows/dependency-submission.yml | 124 +++++++++++++++++ .gitignore | 3 + docs/DEPENDENCY_SECURITY.md | 139 ++++++++++++++++++++ 5 files changed, 319 insertions(+) create mode 100644 .github/workflows/dependency-review.yml create mode 100644 .github/workflows/dependency-submission.yml create mode 100644 docs/DEPENDENCY_SECURITY.md diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8644041..268dfc0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -15,6 +15,15 @@ updates: schedule: interval: "weekly" + # Android build: Gradle/Kotlin/Android plugin versions in settings.gradle.kts. + # Complements (does not replace) the dependency-graph submission in + # .github/workflows/dependency-submission.yml, which is what makes alerts + # cover resolved native and transitive Maven dependencies. + - package-ecosystem: "gradle" + directory: "/apps/playground/android" + schedule: + interval: "weekly" + - package-ecosystem: "github-actions" directory: "/" schedule: diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..031903c --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,44 @@ +name: Dependency Review + +# Blocks pull requests that introduce a dependency with a known High or +# Critical vulnerability, comparing the PR's dependency-graph manifests with the +# base branch. +# +# Coverage: Pub packages (pubspec.lock). Gradle/Maven changes are not covered +# here because dependency snapshots are only submitted for the default branch; +# see "Limitations" in docs/DEPENDENCY_SECURITY.md. Read-only; findings are +# never auto-dismissed. + +on: + pull_request: + types: [opened, synchronize, reopened] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + dependency-review: + name: Dependency Review + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Harden the runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Dependency review + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: high + license-check: false diff --git a/.github/workflows/dependency-submission.yml b/.github/workflows/dependency-submission.yml new file mode 100644 index 0000000..2fd7462 --- /dev/null +++ b/.github/workflows/dependency-submission.yml @@ -0,0 +1,124 @@ +name: Dependency Submission + +# Gives GitHub's dependency graph (and so Dependabot alerts) the native Android +# libraries that apps built with the SDK pull in. +# +# GitHub detects Pub packages from pubspec.lock automatically, but the Maven +# artifacts behind the Flutter plugins' Android code are only known after Gradle +# resolves the playground build. This workflow resolves the release runtime +# classpath of the playground app, sanity-checks the snapshot, keeps it as an +# artifact, and submits it. Build tooling (Android Gradle Plugin internals, +# plugin classpaths) is deliberately left out. See docs/DEPENDENCY_SECURITY.md. +# +# push to main + weekly: submit. workflow_dispatch from another branch: dry run. + +on: + workflow_dispatch: + push: + branches: + - main + schedule: + - cron: "17 5 * * 1" + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +permissions: {} + +jobs: + submit: + name: Submit Gradle dependency snapshot + runs-on: ubuntu-latest + timeout-minutes: 30 + permissions: + contents: write + + steps: + - name: Harden the runner + uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1 + with: + egress-policy: audit + + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + # The playground build targets Java 17. + - name: Set up Java + uses: actions/setup-java@de7274f081f381c8f8158605e0321c36c376e2e6 # v6.0.1 + with: + distribution: temurin + java-version: "17" + + - name: Set up Flutter + uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0 + with: + channel: stable + flutter-version-file: .fvmrc + cache: true + + # Writes apps/playground/android/local.properties (flutter.sdk) and + # .flutter-plugins-dependencies, which Gradle needs to include the Flutter + # plugins' Android projects. + - name: Install dependencies + run: make deps-lockfile + + - name: Validate Gradle wrapper + uses: gradle/actions/wrapper-validation@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + + # `generate` only records the resolved dependencies; submission is done + # below, after the snapshot is checked. + - name: Generate dependency snapshot + uses: gradle/actions/dependency-submission@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0 + with: + build-root-directory: apps/playground/android + dependency-graph: generate + dependency-graph-include-projects: ":app" + dependency-graph-include-configurations: "^releaseRuntimeClasspath$" + cache-disabled: true + + # Fail instead of submitting an empty, partial or mis-attributed snapshot. + - name: Validate snapshot + id: validate + run: | + shopt -s nullglob + files=(dependency-graph-reports/*.json) + if [ "${#files[@]}" -ne 1 ]; then + echo "::error::Expected exactly one dependency snapshot, found ${#files[@]}." + exit 1 + fi + f="${files[0]}" + snap_sha=$(jq -r '.sha' "$f") + snap_ref=$(jq -r '.ref' "$f") + count=$(jq '[.manifests[].resolved | length] | add // 0' "$f") + webkit=$(jq '[.manifests[].resolved[] | select(.package_url | startswith("pkg:maven/androidx.webkit/webkit@"))] | length' "$f") + echo "snapshot=$f" >> "$GITHUB_OUTPUT" + echo "Snapshot $f: sha=$snap_sha ref=$snap_ref components=$count" + if [ "$snap_sha" != "$GITHUB_SHA" ] || [ "$snap_ref" != "$GITHUB_REF" ]; then + echo "::error::Snapshot is for $snap_ref@$snap_sha, expected $GITHUB_REF@$GITHUB_SHA." + exit 1 + fi + # A real build resolves ~90 components; androidx.webkit comes from + # webview_flutter_android, the SDK's WebView dependency. + if [ "$count" -lt 50 ] || [ "$webkit" -lt 1 ]; then + echo "::error::Snapshot looks empty or partial (components=$count, androidx.webkit=$webkit)." + exit 1 + fi + + - name: Upload snapshot + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dependency-snapshot-${{ github.sha }} + path: ${{ steps.validate.outputs.snapshot }} + retention-days: 90 + + - name: Submit snapshot + if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch) + env: + GH_TOKEN: ${{ github.token }} + SNAPSHOT: ${{ steps.validate.outputs.snapshot }} + run: | + gh api --method POST "repos/$GITHUB_REPOSITORY/dependency-graph/snapshots" --input "$SNAPSHOT" + echo "Submitted $SNAPSHOT for $GITHUB_SHA" >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitignore b/.gitignore index 5b8a52e..b049d44 100644 --- a/.gitignore +++ b/.gitignore @@ -38,6 +38,9 @@ coverage/ # Test artifacts (machine reports from `make test-sdk-machine`) test-results/ +# Gradle dependency-graph snapshot (generated by the Dependency Submission workflow) +dependency-graph-reports/ + # Android **/android/.gradle/ **/android/local.properties diff --git a/docs/DEPENDENCY_SECURITY.md b/docs/DEPENDENCY_SECURITY.md new file mode 100644 index 0000000..4addc81 --- /dev/null +++ b/docs/DEPENDENCY_SECURITY.md @@ -0,0 +1,139 @@ +# Dependency security + +Which dependencies of the Formbricks Flutter SDK are scanned for known +vulnerabilities, how, and what to do with a finding. + +## At a glance + +| Question | Answer | +| --------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Does the published `formbricks` package ship native code? | No. It is Dart-only (`flutter pub publish --dry-run` lists `lib/`, docs and metadata; no `android/` or `ios/`). Native code comes from the plugins it depends on, in the consumer's app. | +| Where do Pub vulnerabilities come from? | GitHub's dependency graph reads the root `pubspec.lock`; Dependabot alerts cover all hosted packages. | +| Where do native Android vulnerabilities come from? | The `Dependency Submission` workflow submits the playground app's release runtime classpath to the dependency graph, so Dependabot alerts cover it. | +| What is deliberately not covered? | Gradle build tooling (Android Gradle Plugin internals, plugin classpaths). See [Scope](#scope). | +| Is anything dismissed automatically? | No. Alert dismissal is a human decision in GitHub. | + +## Inventory + +### SDK runtime (what consumers get) + +`packages/formbricks/pubspec.yaml` depends on `http`, `shared_preferences`, +`clock`, `webview_flutter`, `webview_flutter_android`, `url_launcher` and +`connectivity_plus`. Everything resolves through the single workspace lockfile +`pubspec.lock`, which GitHub's dependency graph parses automatically. + +Four of those plugins ship Android code with their own Maven dependencies +(`connectivity_plus`, `shared_preferences_android`, `url_launcher_android`, +`webview_flutter_android`: AndroidX libraries such as `androidx.webkit`, +`androidx.core`, `androidx.datastore`). An app that uses the SDK resolves these +in its own Gradle build, together with its other dependencies, so its final +versions can differ from ours. The playground app's resolution is the closest +proxy we have. + +### Native + +| Platform | Dependencies | Coverage | +| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | +| Android | `apps/playground/android` (Gradle, Android Gradle Plugin, Kotlin) plus the plugin projects above. About 90 Maven artifacts on the release runtime classpath. | `Dependency Submission` workflow (see below) | +| iOS | Swift Package Manager (generated `FlutterGeneratedPluginSwiftPackage`); no `Podfile`, no committed `Package.resolved`. The four iOS plugins' `Package.swift` files depend on nothing third-party (only Flutter's own `FlutterFramework`). | Nothing to submit; no third-party iOS libraries | + +### Build, test and publish + +| Item | Where | Covered by | +| --------------------------------- | --------------------------------------------------- | --------------------------------------------- | +| Pub dev tooling (Melos, lints, …) | `pubspec.lock` | GitHub dependency graph (Pub) | +| GitHub Actions | `.github/workflows` (pinned to commit SHAs) | Dependabot `github-actions` | +| Gradle plugin versions | `apps/playground/android/settings.gradle.kts` | Dependabot `gradle` | +| Gradle wrapper | `gradle-wrapper.jar` | Wrapper validation in `Dependency Submission` | +| Flutter SDK, `pana`, `prettier` | `.fvmrc`; `make pana-install`; `npx prettier@3.6.2` | Not scanned (CI-only, or pinned by hand) | + +`apps/playground` is `publish_to: none`; it is not part of the pub.dev release. +The release workflow formats, analyzes, tests and publishes the Dart package and +does not run Gradle. + +## How Android coverage works + +`.github/workflows/dependency-submission.yml` runs on pushes to `main`, weekly +(to refresh the resolution) and on demand: + +1. Sets up JDK 17 and the pinned Flutter SDK, and runs `make deps-lockfile` + (Gradle needs the `local.properties` and plugin list it writes). +2. Validates the Gradle wrapper, then has Gradle resolve the playground build and + records the result with the + [Gradle dependency-submission action](https://github.com/gradle/actions/blob/main/docs/dependency-submission.md). +3. Checks the snapshot and fails the run instead of submitting if it is for a + different commit or ref, contains fewer than 50 components, or lacks + `androidx.webkit` (the SDK's WebView dependency). +4. Uploads the snapshot as `dependency-snapshot-` (90 days), which ties + exactly what was submitted to the commit. +5. On the default branch, submits it to GitHub's Dependency Submission API. + Run from another branch, the workflow stops after step 4 (a dry run). + +A failed run means the Android part of the graph may be stale. Treat a failing +`Dependency Submission` run as an open security task. + +## Scope + +The snapshot contains only the playground app's `releaseRuntimeClasspath` +(`dependency-graph-include-projects: ":app"` and +`dependency-graph-include-configurations: "^releaseRuntimeClasspath$"`). That is +the native code that is compiled into an app, which is what an SDK user is +exposed to. Debug and test scaffolding and Gradle's own build tooling are +excluded, so they raise no alerts. + +At the time this was set up, the excluded build tooling included advisories +rated High and Critical in Android Gradle Plugin internals (netty, bouncycastle +and similar). They affect the machines that build the app, not the built app. +If build-environment exposure becomes a concern, remove the +`dependency-graph-include-configurations` input; expect many alerts that are +resolved only by upgrading the Android Gradle Plugin. + +## Handling findings + +Findings appear as Dependabot alerts (Security tab) for the default branch, and +as a failing `Dependency Review` check when a pull request introduces a +High/Critical vulnerability in Pub packages. + +For each High/Critical alert, a person decides: + +1. **Locate it.** For a Maven artifact, from `apps/playground/android` after + `make deps-lockfile`: + + ```bash + ./gradlew :app:dependencyInsight --configuration releaseRuntimeClasspath --dependency + ``` + + For a Pub package, check `pubspec.lock` (`direct main`, `direct dev` or + `transitive`). + +2. **Assess reachability.** It is a runtime exposure for SDK users if it is on + the release runtime classpath or a `direct main`/transitive Pub dependency of + `formbricks`. Dev-only packages affect our tooling, not consumers. +3. **Fix or accept.** Bump the dependency (a Dependabot PR, a plugin update, or a + Gradle dependency constraint), or dismiss the alert in GitHub with an accurate + reason and a note on who decided. Nothing in this repository dismisses or + accepts a finding automatically. + +## Before a release + +Dependabot alerts describe the default branch, so check them at the commit you +release: + +1. The latest `Dependency Submission` run on `main` is green and less than a week + old (the weekly run keeps it fresh), so the Android graph is current. +2. No open High/Critical Dependabot alerts remain for the Pub or Gradle + ecosystems, or each is a recorded decision from [Handling findings](#handling-findings). + +## Limitations + +- **Default-branch snapshots only.** `Dependency Review` sees Pub changes on pull + requests but not Gradle changes. Submitting snapshots for pull requests needs a + privileged job that verifies the snapshot's commit and ref against the pull + request, since a forged snapshot could overwrite the default-branch graph. +- **Consumers' builds differ.** The graph reflects the playground's resolution, + not a particular customer's app. +- **iOS** has no third-party native dependencies today. If a plugin adds a + CocoaPod or Swift package, commit `Podfile.lock` or `Package.resolved` so the + graph can see it. +- **Not scanned:** the Flutter SDK, `pana`, `prettier` and its npm dependencies, + and CI runner images.