diff --git a/.github/security-audit/collect-lockfiles.sh b/.github/security-audit/collect-lockfiles.sh new file mode 100755 index 00000000..ddc08128 --- /dev/null +++ b/.github/security-audit/collect-lockfiles.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash +# Used by .github/workflows/security-audit.yml after a resolve step. +# +# Copies every lockfile the resolve step generated into resolved/, keeping +# its repo path, so the osv-scanner job can lay them back over a fresh +# checkout. Most are gitignored (pubspec.lock, composer.lock, Gemfile.lock); +# the rest are merely untracked (requirements.txt, gradle.lockfile, +# Package.resolved), so both listings are needed. +set -euo pipefail + +names='(^|/)(pubspec\.lock|composer\.lock|Gemfile\.lock|requirements\.txt|gradle\.lockfile|Package\.resolved)$' +{ + git ls-files --others --ignored --exclude-standard | grep -E "$names" || true + git ls-files --others --exclude-standard | grep -E "$names" || true +} | grep -v -E '(^|/)(\.build|build|node_modules)/' | sort -u > /tmp/locks.txt + +if [ ! -s /tmp/locks.txt ]; then + echo "::error::no lockfiles were generated" + exit 1 +fi + +while read -r f; do + mkdir -p "resolved/$(dirname "$f")" + cp "$f" "resolved/$f" +done < /tmp/locks.txt + +echo "Generated lockfiles:" +cat /tmp/locks.txt diff --git a/.github/security-audit/lock-all.init.gradle b/.github/security-audit/lock-all.init.gradle new file mode 100644 index 00000000..6b0050db --- /dev/null +++ b/.github/security-audit/lock-all.init.gradle @@ -0,0 +1,34 @@ +// Gradle init script used by .github/workflows/security-audit.yml. +// +// antd-java and antd-kotlin don't commit Gradle lockfiles, so osv-scanner +// has nothing to read. This script turns on dependency locking for every +// project from the outside (no build-file change) and registers a task that +// resolves every resolvable configuration, so +// +// ./gradlew --init-script lock-all.init.gradle resolveAndLockAll --write-locks +// +// writes a gradle.lockfile per project listing the exact versions a fresh +// build resolves today, test and example configurations included. The +// workflow then scans those lockfiles. +// +// -PauditConfigurations=a,b limits resolution to the named configurations. +// ffi/android uses it (releaseRuntimeClasspath, what the published AAR's +// consumers get): resolving every configuration of an Android library also +// pulls in Android build tooling that isn't part of the shipped artifact. + +allprojects { + dependencyLocking { + lockAllConfigurations() + } + + tasks.register("resolveAndLockAll") { + notCompatibleWithConfigurationCache("resolves configurations at execution time") + doFirst { + assert gradle.startParameter.writeDependencyLocks + def only = project.findProperty("auditConfigurations")?.toString()?.split(",")*.trim() + configurations + .findAll { it.canBeResolved && (only == null || only.contains(it.name)) } + .each { it.resolve() } + } + } +} diff --git a/.github/security-audit/nuget-audit.sh b/.github/security-audit/nuget-audit.sh new file mode 100755 index 00000000..90b5c211 --- /dev/null +++ b/.github/security-audit/nuget-audit.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# Used by .github/workflows/security-audit.yml (dotnet-audit job). +# +# Lists vulnerable NuGet packages, transitive included, for each project. +# osv-scanner only sees the direct PackageReferences in a .csproj, so this +# is the transitive check for .NET. +# +# Exit status: 1 if any blocking project has a vulnerable package OR could +# not be audited (restore or listing failed); 0 otherwise. Report-only +# projects (examples) only ever produce warnings. +# +# Every failure is checked explicitly instead of relying on `set -e`: bash +# disables errexit inside a function called from `f || ...`, so a failed +# restore used to fall through to the final grep and pass as clean. +# +# Tests: .github/security-audit/test-nuget-audit.sh (stub dotnet via DOTNET, +# project lists via NUGET_AUDIT_BLOCKING / NUGET_AUDIT_REPORT_ONLY). +set -uo pipefail + +DOTNET="${DOTNET:-dotnet}" + +blocking=( + antd-csharp/Antd.Sdk/Antd.Sdk.csproj + antd-csharp/Antd.Sdk.Tests/Antd.Sdk.Tests.csproj + ffi/csharp/AntFfi/AntFfi.csproj + ffi/csharp/AntFfi.Tests/AntFfi.Tests.csproj +) +report_only=( + antd-csharp/Examples/Examples.csproj +) +if [ -n "${NUGET_AUDIT_BLOCKING+set}" ]; then + read -r -a blocking <<< "$NUGET_AUDIT_BLOCKING" +fi +if [ -n "${NUGET_AUDIT_REPORT_ONLY+set}" ]; then + read -r -a report_only <<< "$NUGET_AUDIT_REPORT_ONLY" +fi + +out="$(mktemp)" +trap 'rm -f "$out"' EXIT + +# 0 = clean, 1 = vulnerable packages found, 2 = the audit could not run. +check() { + local project="$1" list_rc=0 + if ! "$DOTNET" restore "$project" >/dev/null; then + echo "dotnet restore failed for $project" >&2 + return 2 + fi + "$DOTNET" list "$project" package --vulnerable --include-transitive >"$out" || list_rc=$? + cat "$out" + if grep -q "has the following vulnerable packages" "$out"; then + return 1 + fi + if [ "$list_rc" -ne 0 ]; then + echo "dotnet list package failed for $project (exit $list_rc)" >&2 + return 2 + fi + return 0 +} + +failed=0 +for p in ${blocking[@]+"${blocking[@]}"}; do + echo "::group::$p" + rc=0 + check "$p" || rc=$? + echo "::endgroup::" + case "$rc" in + 0) ;; + 1) echo "::error file=$p::vulnerable NuGet packages"; failed=1 ;; + *) echo "::error file=$p::NuGet audit could not run (restore or package listing failed)"; failed=1 ;; + esac +done +for p in ${report_only[@]+"${report_only[@]}"}; do + echo "::group::$p (report only)" + rc=0 + check "$p" || rc=$? + echo "::endgroup::" + case "$rc" in + 0) ;; + 1) echo "::warning file=$p::vulnerable NuGet packages (examples are report-only)" ;; + *) echo "::warning file=$p::NuGet audit could not run (examples are report-only)" ;; + esac +done +exit "$failed" diff --git a/.github/security-audit/test-nuget-audit.sh b/.github/security-audit/test-nuget-audit.sh new file mode 100755 index 00000000..7de57a93 --- /dev/null +++ b/.github/security-audit/test-nuget-audit.sh @@ -0,0 +1,60 @@ +#!/usr/bin/env bash +# Regression tests for nuget-audit.sh, run by security-audit.yml before the +# real audit. A stub `dotnet` returns controlled exit codes and output, so +# every path is exercised without the .NET SDK, including the ones that +# used to pass as clean (restore or listing failures). +set -euo pipefail + +here="$(cd "$(dirname "$0")" && pwd)" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT + +cat > "$tmp/dotnet" <<'STUB' +#!/usr/bin/env bash +case "$1" in + restore) exit "${STUB_RESTORE_RC:-0}" ;; + list) + if [ "${STUB_LIST_OUT:-clean}" = vuln ]; then + echo "Project \`Fake\` has the following vulnerable packages" + echo " > Some.Package 1.0.0 High https://github.com/advisories/GHSA-xxxx" + else + echo "The given project \`Fake\` has no vulnerable packages given the current sources." + fi + exit "${STUB_LIST_RC:-0}" ;; +esac +exit 99 +STUB +chmod +x "$tmp/dotnet" + +fails=0 +# run [VAR=value ...] +run() { + local name="$1" want="$2" grepfor="$3"; shift 3 + local got=0 + env DOTNET="$tmp/dotnet" "$@" bash "$here/nuget-audit.sh" >"$tmp/out" 2>&1 || got=$? + if [ "$got" -ne "$want" ]; then + echo "FAIL $name: exit $got, want $want"; cat "$tmp/out"; fails=$((fails + 1)); return + fi + if [ -n "$grepfor" ] && ! grep -q -- "$grepfor" "$tmp/out"; then + echo "FAIL $name: output lacks '$grepfor'"; cat "$tmp/out"; fails=$((fails + 1)); return + fi + echo "ok $name (exit $got)" +} + +B=(NUGET_AUDIT_BLOCKING=Fake.csproj NUGET_AUDIT_REPORT_ONLY=) +R=(NUGET_AUDIT_BLOCKING= NUGET_AUDIT_REPORT_ONLY=Example.csproj) + +run "blocking: clean" 0 "" "${B[@]}" +run "blocking: vulnerable" 1 "::error file=Fake.csproj::vulnerable" "${B[@]}" STUB_LIST_OUT=vuln +run "blocking: restore fails" 1 "could not run" "${B[@]}" STUB_RESTORE_RC=17 +run "blocking: list fails" 1 "could not run" "${B[@]}" STUB_LIST_RC=23 +run "blocking: restore and list fail" 1 "could not run" "${B[@]}" STUB_RESTORE_RC=17 STUB_LIST_RC=23 +run "blocking: list fails, vulns printed" 1 "vulnerable NuGet packages" "${B[@]}" STUB_LIST_RC=1 STUB_LIST_OUT=vuln +run "report-only: vulnerable warns" 0 "::warning file=Example.csproj::vulnerable" "${R[@]}" STUB_LIST_OUT=vuln +run "report-only: restore fails warns" 0 "::warning file=Example.csproj::NuGet audit could not run" "${R[@]}" STUB_RESTORE_RC=17 + +if [ "$fails" -ne 0 ]; then + echo "$fails nuget-audit.sh test(s) failed" + exit 1 +fi +echo "all nuget-audit.sh tests passed" diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 00000000..7e1a1d2a --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,448 @@ +name: Security audit + +# Dependency advisory scan for every SDK in the repo (V2-1582 ยง3). +# +# Runs Monday and Wednesday at 05:17 UTC, ahead of the release-decision +# meetings, so a new advisory against an unchanged lockfile shows up without +# waiting for someone to touch that SDK. It also runs on any PR or main push +# that changes a manifest or lockfile, and on demand (workflow_dispatch). +# +# Jobs: +# cargo-audit RustSec advisories for every Cargo.lock. Honours each +# directory's .cargo/audit.toml. Fails on vulnerabilities, +# warns on unmaintained/unsound/yanked. +# govulncheck Go vulnerabilities antd-go actually calls (reachability). +# npm-audit npm advisories from the committed package-lock.json files. +# dotnet-audit NuGet advisories, transitive included (osv-scanner only +# sees the direct PackageReferences in a .csproj). +# resolve Most SDKs deliberately don't commit lockfiles (they're +# libraries). This job generates the lockfile a fresh +# install would get today: Dart, PHP, Ruby, Python, Gradle +# (antd-java, antd-kotlin, ffi/android, ffi/kotlin) and +# Swift. ffi/python and ffi/swift declare no package +# dependencies (their native library is ffi/rust, which +# cargo-audit and osv-scanner cover), so there is nothing +# to resolve for them. +# osv-scanner Scans every committed and generated lockfile against +# OSV, which also carries GitHub-only advisories that +# RustSec / cargo audit miss. Blocking for everything +# except examples/; the examples get a report-only pass. +# +# Policy: any finding outside examples/ fails the run, dev and test +# dependencies included (they run in CI next to publishing credentials). +# Accepted findings go in osv-scanner.toml (Rust: also .cargo/audit.toml) +# with a reason, a tracking issue and an ignoreUntil date. Ecosystems without +# a committed lockfile are scanned as resolved on the day, so results can +# change between runs without a code change. That's the point of the +# schedule. Lua, Zig and C++ have no usable advisory database and aren't +# scanned. + +on: + schedule: + - cron: "17 5 * * 1,3" + workflow_dispatch: + push: + branches: [main] + paths: + - ".github/workflows/security-audit.yml" + - ".github/security-audit/**" + - "osv-scanner.toml" + - "**/.cargo/audit.toml" + - "**/Cargo.lock" + - "**/Cargo.toml" + - "**/go.mod" + - "**/go.sum" + - "**/package.json" + - "**/package-lock.json" + - "**/pyproject.toml" + - "**/pubspec.yaml" + - "**/composer.json" + - "**/Gemfile" + - "**/*.gemspec" + - "**/mix.exs" + - "**/mix.lock" + - "**/*.gradle.kts" + - "**/Package.swift" + - "**/*.csproj" + pull_request: + paths: + - ".github/workflows/security-audit.yml" + - ".github/security-audit/**" + - "osv-scanner.toml" + - "**/.cargo/audit.toml" + - "**/Cargo.lock" + - "**/Cargo.toml" + - "**/go.mod" + - "**/go.sum" + - "**/package.json" + - "**/package-lock.json" + - "**/pyproject.toml" + - "**/pubspec.yaml" + - "**/composer.json" + - "**/Gemfile" + - "**/*.gemspec" + - "**/mix.exs" + - "**/mix.lock" + - "**/*.gradle.kts" + - "**/Package.swift" + - "**/*.csproj" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + # Scanner versions are pinned so a scanner release can't change results + # unannounced. Bump them deliberately. + CARGO_AUDIT_VERSION: "0.22.2" + GOVULNCHECK_VERSION: "v1.9.0" + OSV_SCANNER_VERSION: "2.7.0" + UV_VERSION: "0.13.3" + +jobs: + cargo-audit: + name: cargo audit (${{ matrix.dir }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dir: [antd, antd-rust, ffi/rust, ffi/node] + steps: + - uses: actions/checkout@v4 + + - uses: Swatinem/rust-cache@v2 + with: + # Same key as ci.yml's audit job: only the cargo-audit binary is + # cached, not any crate's build. + key: cargo-audit-tool + + - name: Install cargo-audit + run: cargo install cargo-audit --locked --version "$CARGO_AUDIT_VERSION" + + - name: cargo audit + working-directory: ${{ matrix.dir }} + run: cargo audit + + govulncheck: + name: govulncheck (antd-go) + runs-on: ubuntu-latest + defaults: + run: + working-directory: antd-go + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-go@v5 + with: + go-version-file: antd-go/go.mod + cache-dependency-path: antd-go/go.sum + + - name: Install govulncheck + run: go install "golang.org/x/vuln/cmd/govulncheck@$GOVULNCHECK_VERSION" + + # Fails only on vulnerabilities in code antd-go actually calls. + # Module-level findings it can't reach are listed but don't fail; the + # osv-scanner job reports those against go.mod. + - name: govulncheck + run: govulncheck ./... + + npm-audit: + name: npm audit (${{ matrix.dir }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + dir: [antd-js, ffi/node] + defaults: + run: + working-directory: ${{ matrix.dir }} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + + # Reads package-lock.json directly; nothing is installed. + - name: npm audit (all dependencies, dev included) + run: npm audit --package-lock-only --audit-level=low + + dotnet-audit: + name: NuGet audit (.NET) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-dotnet@v4 + with: + dotnet-version: "8.0.x" + + # Exercises every exit path of the audit script against a stub dotnet, + # including restore/listing failures that must not pass as clean. + - name: Test the NuGet audit script + run: bash .github/security-audit/test-nuget-audit.sh + + - name: Vulnerable packages (transitive included) + run: bash .github/security-audit/nuget-audit.sh + + resolve: + name: Resolve lockfiles (${{ matrix.ecosystem }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ecosystem: [dart, php, ruby, python, gradle] + steps: + - uses: actions/checkout@v4 + + - if: matrix.ecosystem == 'dart' + uses: dart-lang/setup-dart@v1 + with: + sdk: stable + + - if: matrix.ecosystem == 'dart' + name: dart pub get + working-directory: antd-dart + run: dart pub get + + - if: matrix.ecosystem == 'php' + uses: shivammathur/setup-php@v2 + with: + php-version: "8.4" + coverage: none + tools: composer:v2 + + # Writes composer.lock without installing anything. Platform + # requirements (gmp for the external-signer dev dependency) don't + # matter for resolution. + - if: matrix.ecosystem == 'php' + name: composer update --no-install + working-directory: antd-php + run: composer update --no-install --no-interaction --no-progress --no-plugins --no-scripts --ignore-platform-reqs + + - if: matrix.ecosystem == 'ruby' + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.3" + + - if: matrix.ecosystem == 'ruby' + name: bundle lock + working-directory: antd-ruby + run: bundle lock + + - if: matrix.ecosystem == 'python' + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + # antd-mcp and ant-dev depend on antd from PyPI, as a user's install + # would. --all-extras covers rest, grpc and dev. + - if: matrix.ecosystem == 'python' + name: uv pip compile + run: | + set -euo pipefail + python -m pip install --disable-pip-version-check "uv==$UV_VERSION" + for p in antd-py antd-mcp ant-dev; do + uv pip compile "$p/pyproject.toml" --all-extras --quiet -o "$p/requirements.txt" + done + + - if: matrix.ecosystem == 'gradle' + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + + # ffi/android and ffi/kotlin have no wrapper; ffi-ci.yml builds + # ffi/android with Gradle 8.9. + - if: matrix.ecosystem == 'gradle' + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.9" + + # Four separate Gradle builds; none includes another, so each is locked + # on its own. ffi/kotlin is the standalone JVM UniFFI binding (JNA, + # coroutines). + - if: matrix.ecosystem == 'gradle' + name: Gradle dependency locking (antd-java, antd-kotlin, ffi/android, ffi/kotlin) + env: + INIT: ${{ github.workspace }}/.github/security-audit/lock-all.init.gradle + run: | + set -euo pipefail + (cd antd-java && ./gradlew --no-daemon --console=plain --init-script "$INIT" resolveAndLockAll --write-locks) + (cd antd-kotlin && ../antd-java/gradlew --no-daemon --console=plain -p . --init-script "$INIT" resolveAndLockAll --write-locks) + (cd ffi/android && gradle --no-daemon --console=plain --init-script "$INIT" resolveAndLockAll --write-locks \ + -PauditConfigurations=releaseRuntimeClasspath) + (cd ffi/kotlin && gradle --no-daemon --console=plain --init-script "$INIT" resolveAndLockAll --write-locks) + # Each Gradle root must have produced a lockfile; a silently + # skipped root would mean an unscanned SDK. + for f in antd-java/gradle.lockfile antd-kotlin/lib/gradle.lockfile \ + ffi/android/gradle.lockfile ffi/kotlin/AntFfi/gradle.lockfile; do + test -s "$f" || { echo "::error file=$f::expected Gradle lockfile was not generated"; exit 1; } + done + + # Copy every generated lockfile under resolved/, keeping its repo path, + # so the osv-scanner job can lay them back over a fresh checkout. + - name: Collect generated lockfiles + run: .github/security-audit/collect-lockfiles.sh + + - uses: actions/upload-artifact@v4 + with: + name: resolved-${{ matrix.ecosystem }} + path: resolved/ + if-no-files-found: error + retention-days: 7 + + # Swift resolves in its own job because the toolchain decides what gets + # resolved: SwiftNIO's security fixes (swift-nio >= 2.100.0, -http2 >= + # 1.44.0, -extras >= 1.34.1) need swift-tools-version 6.1, so a Swift 6.0 + # toolchain silently resolves older, vulnerable releases. The official + # image pins a current toolchain (what someone installing antd-swift today + # would use); setup-swift lags behind Swift releases. ffi/swift has no + # package dependencies, so only antd-swift has anything to resolve. + resolve-swift: + name: Resolve lockfiles (swift) + runs-on: ubuntu-latest + container: swift:6.4 + steps: + - uses: actions/checkout@v4 + + - name: swift package resolve + working-directory: antd-swift + run: | + swift --version + swift package resolve + + # In a container the workspace belongs to the runner's user, not the + # container's, so git refuses it ("dubious ownership") unless told + # it's safe. checkout's own safe.directory entry doesn't outlive that step. + - name: Collect generated lockfiles + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + .github/security-audit/collect-lockfiles.sh + + - uses: actions/upload-artifact@v4 + with: + name: resolved-swift + path: resolved/ + if-no-files-found: error + retention-days: 7 + + osv-scanner: + name: osv-scanner (all lockfiles) + needs: [resolve, resolve-swift] + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + pattern: resolved-* + merge-multiple: true + path: . + + # A resolve job that silently produced fewer lockfiles would leave an + # SDK unscanned while the scan still passed, so every expected source + # is checked. Add new SDKs/manifests here. + - name: Check lockfile coverage + run: | + set -euo pipefail + expected=( + # committed + antd/Cargo.lock antd-rust/Cargo.lock ffi/rust/Cargo.lock ffi/node/Cargo.lock + antd-go/go.mod antd-js/package-lock.json ffi/node/package-lock.json antd-elixir/mix.lock + # generated by the resolve jobs + antd-dart/pubspec.lock antd-php/composer.lock antd-ruby/Gemfile.lock + antd-py/requirements.txt antd-mcp/requirements.txt ant-dev/requirements.txt + antd-java/gradle.lockfile antd-kotlin/lib/gradle.lockfile + ffi/android/gradle.lockfile ffi/kotlin/AntFfi/gradle.lockfile + antd-swift/Package.resolved + ) + missing=0 + { + echo "## Lockfiles scanned (${#expected[@]} expected)" + echo + for f in "${expected[@]}"; do + if [ -s "$f" ]; then echo "- \`$f\`"; else echo "- **MISSING** \`$f\`"; missing=1; fi + done + echo + } >> "$GITHUB_STEP_SUMMARY" + for f in "${expected[@]}"; do + [ -s "$f" ] || echo "::error file=$f::expected lockfile is missing, so this SDK would not be scanned" + done + exit "$missing" + + - name: Install osv-scanner + env: + GH_TOKEN: ${{ github.token }} + run: | + set -euo pipefail + cd "$RUNNER_TEMP" + gh release download "v$OSV_SCANNER_VERSION" --repo google/osv-scanner \ + --pattern osv-scanner_linux_amd64 --pattern osv-scanner_SHA256SUMS + grep ' osv-scanner_linux_amd64$' osv-scanner_SHA256SUMS | sha256sum -c - + install -m 0755 osv-scanner_linux_amd64 /usr/local/bin/osv-scanner + osv-scanner --version + + # --no-ignore: the generated lockfiles are gitignored (the SDKs don't + # commit them), so osv-scanner would otherwise skip them. + # + # Exit codes are captured with `|| code=$?`: Actions runs bash with -e, + # so a bare non-zero osv-scanner call (exit 1 = findings) would end the + # step before the summary and the ::error:: line are written. + - name: Scan (blocking; everything except examples) + run: | + set -uo pipefail + args=(scan source --recursive --no-ignore --config osv-scanner.toml + --x-exclude 'g:**/examples' --x-exclude 'g:**/Examples') + osv-scanner "${args[@]}" --format sarif --output-file osv.sarif . >/dev/null 2>&1 || true + code=0 + osv-scanner "${args[@]}" --format markdown --output-file osv.md . 2>osv.log || code=$? + { + echo "## osv-scanner: blocking pass (everything except examples)" + echo + if [ "$code" -eq 0 ]; then echo "No unaccepted findings."; else cat osv.md; fi + echo + echo "
Accepted findings (osv-scanner.toml)" + echo + grep 'has been filtered out because' osv.log | sort -u | sed 's/^/- /' || true + echo + echo "
" + } >> "$GITHUB_STEP_SUMMARY" + cat osv.log + [ "$code" -eq 0 ] || cat osv.md + case "$code" in + 0) ;; + 1) echo "::error::osv-scanner found unaccepted vulnerabilities (see the job summary)"; exit 1 ;; + *) echo "::error::osv-scanner failed with exit code $code"; exit "$code" ;; + esac + + - name: Upload SARIF to code scanning + if: always() && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository) + uses: github/codeql-action/upload-sarif@v4 + with: + sarif_file: osv.sarif + category: osv-scanner + + - name: Scan examples (report only) + if: always() + run: | + set -uo pipefail + mapfile -t dirs < <(find . -type d \( -name examples -o -name Examples \) -not -path './.git/*' | sort) + code=0 + osv-scanner scan source --recursive --no-ignore --config osv-scanner.toml \ + --format markdown --output-file osv-examples.md "${dirs[@]}" 2>/dev/null || code=$? + { + echo "## osv-scanner: examples (report only, never fails)" + echo + if [ "$code" -eq 0 ]; then echo "No findings."; else cat osv-examples.md; fi + } >> "$GITHUB_STEP_SUMMARY" + if [ "$code" -eq 1 ]; then + echo "::warning::osv-scanner found vulnerabilities in example projects (report only; see the job summary)" + fi + exit 0 diff --git a/osv-scanner.toml b/osv-scanner.toml new file mode 100644 index 00000000..8b568718 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,110 @@ +# osv-scanner configuration for .github/workflows/security-audit.yml. +# +# Every entry is a reasoned, time-boxed ignore. Do not add one without: +# - why the finding is acceptable (unreachable, no fix exists, ...), +# - where removal is tracked (Linear issue or upstream PR), and +# - an ignoreUntil date. When the date passes the finding is reported +# again and the audit fails, which forces a re-review. +# +# Rust ignores here must agree with the cargo-audit ignores in +# antd/, ffi/rust/ and ffi/node/ .cargo/audit.toml. + +# --- zip 2.4.2 (transitive: ant-core -> zip) -------------------------------- +# All three affect only ZipArchive::extract() / ZipStreamReader::extract(). +# ant-core's node::binary::extract_zip walks entries with by_index and copies +# one named file itself; it never calls extract(), and the SDKs never call +# ant_core::node at all. Same reasoning and date as the cargo-audit ignores +# from #372. Remove when ant-core is repinned to a release with zip >= 9 +# (V2-1634; upstream ant-client#233 / V2-1632). + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0367" +ignoreUntil = 2026-11-16 +reason = "zip extract() SUID/SGID preservation; ant-core never calls extract(). V2-1634." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0368" +ignoreUntil = 2026-11-16 +reason = "zip extract() symlink escape; ant-core never calls extract(). V2-1634." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0369" +ignoreUntil = 2026-11-16 +reason = "zip extract() permissions through symlinks; ant-core never calls extract(). V2-1634." + +# --- lru 0.16 (transitive: evmlib -> alloy -> alloy-provider -> lru) -------- +# Unsound LruCache::pop() under a panicking closure. The fix is lru 0.18, a +# semver-major bump that has to come from alloy-provider upstream. + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0253" +ignoreUntil = 2027-01-18 +reason = "lru 0.16 unsound pop(); fix needs lru 0.18 via an alloy-provider upgrade upstream. V2-1582." + +# --- RustSec "unmaintained" notices ----------------------------------------- +# Informational, not vulnerabilities: cargo audit reports them as warnings and +# does not fail on them, but osv-scanner reports them like any other +# advisory. None has a drop-in replacement we control; all arrive through +# ant-core / saorsa / alloy dependencies. Re-review each quarter. + +[[IgnoredVulns]] +id = "RUSTSEC-2023-0089" +ignoreUntil = 2027-01-18 +reason = "atomic-polyfill unmaintained (informational); transitive. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2025-0141" +ignoreUntil = 2027-01-18 +reason = "bincode 1.x unmaintained (informational); transitive. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2024-0388" +ignoreUntil = 2027-01-18 +reason = "derivative unmaintained (informational); transitive. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2024-0436" +ignoreUntil = 2027-01-18 +reason = "paste unmaintained (informational); transitive. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0173" +ignoreUntil = 2027-01-18 +reason = "proc-macro-error2 unmaintained (informational); transitive, build-time only. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2025-0134" +ignoreUntil = 2027-01-18 +reason = "rustls-pemfile unmaintained (informational); transitive. V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2024-0449" +ignoreUntil = 2027-01-18 +reason = "unsafe-libyaml unmaintained (informational); transitive (ffi). V2-1582." + +[[IgnoredVulns]] +id = "RUSTSEC-2026-0374" +ignoreUntil = 2027-01-18 +reason = "basic-toml unmaintained (informational); build-time only, via uniffi -> uniffi_bindgen -> askama_derive (ffi). V2-1582." + +# --- Go: golang.org/x/crypto/openpgp ---------------------------------------- +# The openpgp package is deprecated and will not be fixed. antd-go does not +# import it (govulncheck: not called, not imported); x/crypto is only required +# for other packages. + +[[IgnoredVulns]] +id = "GO-2026-5932" +ignoreUntil = 2027-01-18 +reason = "x/crypto/openpgp is unmaintained with no fix; antd-go does not import it. V2-1582." + +# --- Elixir: cowlib / gun (transitive: grpc -> gun -> cowlib) ---------------- +# Medium advisory left after #375 moved antd-elixir to grpc 1.0.5. Hermes's +# source review found it unreachable through the SDK's default client path. +# No fixed cowlib release confirmed yet. Tracked in V2-1637 item 4. +# osv-scanner treats GHSA-w4f7-4cxr-rv3c as an alias of this id, so this +# one entry covers both (a separate GHSA entry is reported as unused). + +[[IgnoredVulns]] +id = "EEF-CVE-2026-43966" +ignoreUntil = 2026-12-14 +reason = "cowlib advisory via gun (alias GHSA-w4f7-4cxr-rv3c); unreachable through antd-elixir's client path. V2-1637."