Skip to content
Closed
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
151 changes: 151 additions & 0 deletions .github/actions/gradle-dependency-snapshot/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
name: Gradle dependency snapshot and assessment
description: >-
Resolves the Android/Gradle dependencies of the playground app for the
checked-out commit, validates the resulting dependency-graph snapshot against
the repo and Gradle's own resolution, assesses the full inventory (Gradle + Pub)
against OSV, and uploads the evidence. Never submits anything: the snapshot is
generated with a read-only token and submitted by a separate job.

# Run after actions/checkout and step-security/harden-runner. See
# docs/DEPENDENCY_SECURITY.md.

inputs:
fail-on-findings:
description: >-
"true" fails the step when the complete inventory contains High/Critical
(or unrated) advisories that have no active exception. An incomplete
inventory always fails.
required: false
default: "false"

outputs:
sha:
description: Commit SHA the inventory describes.
value: ${{ steps.rev.outputs.sha }}
evidence-artifact:
description: Name of the uploaded evidence artifact.
value: dependency-evidence-${{ steps.rev.outputs.sha }}

runs:
using: composite
steps:
- name: Resolve commit
id: rev
shell: bash
run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"

# AGP 9 / Gradle 9 need JDK 17+; the playground 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. --enforce-lockfile: resolve exactly what is
# committed.
- name: Install dependencies
shell: bash
run: make deps-lockfile

- name: Validate Gradle wrapper
uses: gradle/actions/wrapper-validation@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0

# `generate` only runs the dependency-graph init script; nothing is
# submitted or uploaded here. Caching is off so no cache entry can carry
# state between runs of this security-relevant job.
- name: Generate dependency snapshot
uses: gradle/actions/dependency-submission@3f5f9adaf7d9fecd50b5935e54106014257a94e6 # v6.4.0
env:
# Bind the snapshot to the checked-out commit, not the workflow's.
GITHUB_DEPENDENCY_GRAPH_SHA: ${{ steps.rev.outputs.sha }}
with:
build-root-directory: apps/playground/android
dependency-graph: generate
cache-disabled: true

# Gradle's own view of what the app runtime classpath resolves to. Used to
# (a) prove the snapshot is not missing resolved libraries and (b) tell
# shipped native libraries apart from build tooling.
- name: Resolve app runtime classpaths
shell: bash
working-directory: apps/playground/android
env:
# The graph init script must not emit a second snapshot for these runs.
GITHUB_DEPENDENCY_GRAPH_ENABLED: "false"
run: |
mkdir -p "$GITHUB_WORKSPACE/dependency-evidence"
for variant in release debug; do
./gradlew -q :app:dependencies --configuration "${variant}RuntimeClasspath" \
> "$GITHUB_WORKSPACE/dependency-evidence/gradle-${variant}-runtime-classpath.txt"
done

- name: Validate snapshot against Gradle resolution
shell: bash
env:
SHA: ${{ steps.rev.outputs.sha }}
run: |
dart run tool/dependency_inventory.dart validate-snapshot \
--sha "$SHA" \
--gradle-snapshot dependency-graph-reports \
--runtime-classpath dependency-evidence/gradle-release-runtime-classpath.txt \
--runtime-classpath dependency-evidence/gradle-debug-runtime-classpath.txt

- name: Assess dependency inventory
id: assess
shell: bash
env:
SHA: ${{ steps.rev.outputs.sha }}
FAIL_ON_FINDINGS: ${{ inputs.fail-on-findings }}
run: |
set +e
dart run tool/dependency_inventory.dart assess \
--sha "$SHA" \
--gradle-snapshot dependency-graph-reports \
--runtime-classpath dependency-evidence/gradle-release-runtime-classpath.txt \
--runtime-classpath dependency-evidence/gradle-debug-runtime-classpath.txt \
--out dependency-evidence
code=$?
set -e
if [ -f dependency-evidence/assessment.md ]; then
cat dependency-evidence/assessment.md >> "$GITHUB_STEP_SUMMARY"
fi
case "$code" in
0) ;;
1)
echo "::warning::High/Critical (or unrated) dependency advisories need human triage - see the job summary and docs/DEPENDENCY_SECURITY.md."
if [ "$FAIL_ON_FINDINGS" = "true" ]; then exit 1; fi
;;
*)
echo "::error::Dependency inventory is INCOMPLETE - this is not a clean result."
exit "$code"
;;
esac

# Self-contained evidence tied to the commit: the snapshot, Gradle's
# runtime classpaths, the full inventory and the assessment.
- name: Collect snapshot into evidence
if: always()
shell: bash
run: |
mkdir -p dependency-evidence/gradle-snapshot
if [ -d dependency-graph-reports ]; then
cp dependency-graph-reports/*.json dependency-evidence/gradle-snapshot/ 2>/dev/null || true
fi

- name: Upload evidence
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: dependency-evidence-${{ steps.rev.outputs.sha }}
path: dependency-evidence/
retention-days: 90
if-no-files-found: warn
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
3 changes: 3 additions & 0 deletions .github/dependency-exceptions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"exceptions": []
}
43 changes: 43 additions & 0 deletions .github/workflows/dependency-review.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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
# yet — snapshots for pull requests are not generated; see "Follow-ups" 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
105 changes: 105 additions & 0 deletions .github/workflows/dependency-submission.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
name: Dependency Submission

# Android/Gradle dependency coverage for GitHub's dependency graph.
#
# GitHub detects Pub packages from pubspec.lock automatically but cannot see the
# native Android dependencies (Maven artifacts, Gradle/Kotlin/Android build
# plugins), because they are only known after Gradle resolves the playground
# build. This workflow resolves them, checks the snapshot is complete, and
# submits it so Dependabot alerts cover them. See docs/DEPENDENCY_SECURITY.md.
#
# Trigger notes:
# * push to main + weekly: collect, assess, submit the default-branch graph.
# * workflow_dispatch from main: same. From any other branch: collect and
# assess only (a dry run); nothing is submitted.
# * No pull_request trigger on purpose: submitting from PRs needs a
# privileged workflow that handles untrusted snapshots. See the follow-ups
# in the doc.

on:
workflow_dispatch:
push:
branches:
- main
schedule:
- cron: "17 5 * * 1"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions: {}

jobs:
collect:
name: Collect and assess Gradle dependencies
runs-on: ubuntu-latest
timeout-minutes: 45
# Read-only: Gradle runs third-party plugin build scripts here.
permissions:
contents: read

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

# High/Critical findings are surfaced as a warning + job summary here and
# do not fail the default-branch run; an incomplete inventory does.
- name: Collect, validate and assess
uses: ./.github/actions/gradle-dependency-snapshot

submit:
name: Submit dependency snapshot
needs: collect
if: github.ref == format('refs/heads/{0}', github.event.repository.default_branch)
runs-on: ubuntu-latest
timeout-minutes: 10
# The only job with a write token. It runs no repository code: it downloads
# the validated snapshot, re-checks it is bound to this commit and ref, and
# posts it to the Dependency Submission API.
permissions:
contents: write

steps:
- name: Harden the runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

- name: Download evidence
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: dependency-evidence-${{ github.sha }}
path: dependency-evidence

- name: Submit snapshot
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
SHA: ${{ github.sha }}
REF: ${{ github.ref }}
run: |
shopt -s nullglob
files=(dependency-evidence/gradle-snapshot/*.json)
if [ "${#files[@]}" -eq 0 ]; then
echo "::error::No dependency snapshot in the evidence artifact; refusing to submit."
exit 1
fi
for f in "${files[@]}"; do
snap_sha=$(jq -r '.sha' "$f")
snap_ref=$(jq -r '.ref' "$f")
count=$(jq '[.manifests[].resolved | length] | add // 0' "$f")
if [ "$snap_sha" != "$SHA" ] || [ "$snap_ref" != "$REF" ] || [ "$count" -lt 100 ]; then
echo "::error::$f is not a valid snapshot for $REF@$SHA (sha=$snap_sha ref=$snap_ref components=$count)."
exit 1
fi
gh api --method POST "repos/$REPO/dependency-graph/snapshots" --input "$f"
echo "Submitted $f ($count components) for $SHA" >> "$GITHUB_STEP_SUMMARY"
done
54 changes: 54 additions & 0 deletions .github/workflows/release-dependency-assessment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Release Dependency Assessment

# Assess the exact dependency inventory of a release candidate, independent of
# the default branch's Dependabot alert counts. Run it from the Actions tab and
# give the branch, tag or full commit SHA you intend to release.
#
# Collects the Android/Gradle inventory for that commit, adds the Pub lockfile,
# checks every component against OSV, and uploads evidence bound to the SHA
# (artifact `dependency-evidence-<sha>`). The run FAILS when the inventory is
# incomplete or when High/Critical (or unrated) advisories have no active human
# exception. It never submits anything to GitHub and never dismisses or accepts
# a finding. See docs/DEPENDENCY_SECURITY.md.
#
# The candidate must contain this tooling (.github/actions/gradle-dependency-snapshot
# and tool/dependency_inventory.dart); it is checked out and run as-is.

on:
workflow_dispatch:
inputs:
ref:
description: Release candidate branch, tag or full commit SHA
required: true
type: string

concurrency:
group: ${{ github.workflow }}-${{ inputs.ref }}
cancel-in-progress: false

permissions: {}

jobs:
assess:
name: Assess release candidate
runs-on: ubuntu-latest
timeout-minutes: 45
permissions:
contents: read

steps:
- name: Harden the runner
uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
with:
egress-policy: audit

- name: Checkout release candidate
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ inputs.ref }}
persist-credentials: false

- name: Collect, validate and assess
uses: ./.github/actions/gradle-dependency-snapshot
with:
fail-on-findings: "true"
3 changes: 3 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,9 @@ jobs:
DEPS_TARGET: ${{ matrix.deps-target }}
run: make "$DEPS_TARGET"

- name: Run tooling tests
run: make test-tools

- name: Run SDK package tests
run: make test-sdk-machine

Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ coverage/
# Test artifacts (machine reports from `make test-sdk-machine`)
test-results/

# Dependency-assessment output (tool/dependency_inventory.dart, CI evidence)
dependency-evidence/
dependency-graph-reports/

# Android
**/android/.gradle/
**/android/local.properties
Expand Down
Loading
Loading