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.