Skip to content
Merged
Show file tree
Hide file tree
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
9 changes: 9 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
44 changes: 44 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -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
124 changes: 124 additions & 0 deletions .github/workflows/dependency-submission.yml
Original file line number Diff line number Diff line change
@@ -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"
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
139 changes: 139 additions & 0 deletions docs/DEPENDENCY_SECURITY.md
Original file line number Diff line number Diff line change
@@ -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-<sha>` (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 <group:artifact>
```

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