From 0dafee1abc9f22de972167eb65b125235388fd28 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 20 Oct 2026 11:14:16 +0100 Subject: [PATCH 1/5] ci: scheduled security audit across every SDK (Mon + Wed, osv-scanner + native auditors) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds .github/workflows/security-audit.yml (V2-1582 §3). It runs at 05:17 UTC on Mondays and Wednesdays, ahead of the release-decision meetings, on any PR or main push that changes a manifest or lockfile, and on demand. - cargo-audit: every Cargo.lock (antd, antd-rust, ffi/rust, ffi/node), honouring each .cargo/audit.toml. - govulncheck: antd-go, failing only on reachable vulnerabilities. - npm-audit: antd-js and ffi/node, dev dependencies included. - dotnet-audit: NuGet, transitive included; Examples is report-only. - resolve: generates the lockfiles the SDKs deliberately don't commit (Dart, PHP, Ruby, Python via uv, Gradle for Java/Kotlin/Android via an init script, Swift) so they can be scanned as a fresh install would resolve them today. - osv-scanner: every committed and generated lockfile. Blocking for everything except examples/; examples get a report-only pass with a warning. Results go to the job summary and to code scanning as SARIF. osv-scanner.toml holds the accepted findings, each with a reason, tracking issue and ignoreUntil date: zip (until 2026-11-16, matching #372), lru, the RustSec "unmaintained" notices, x/crypto openpgp and the Elixir cowlib advisory (V2-1637). Scanner versions are pinned. .github/security-audit/lock-all.init.gradle enables Gradle dependency locking from outside the build, so no build file changes. Refs V2-1582. Co-Authored-By: Claude Opus 5.5 --- .github/security-audit/lock-all.init.gradle | 34 ++ .github/workflows/security-audit.yml | 407 ++++++++++++++++++++ osv-scanner.toml | 108 ++++++ 3 files changed, 549 insertions(+) create mode 100644 .github/security-audit/lock-all.init.gradle create mode 100644 .github/workflows/security-audit.yml create mode 100644 osv-scanner.toml 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/workflows/security-audit.yml b/.github/workflows/security-audit.yml new file mode 100644 index 00000000..b09219b3 --- /dev/null +++ b/.github/workflows/security-audit.yml @@ -0,0 +1,407 @@ +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 +# (Java, Kotlin, Android) and Swift. +# 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" + + - name: Vulnerable packages (transitive included) + run: | + set -euo pipefail + 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 + ) + check() { + dotnet restore "$1" >/dev/null + dotnet list "$1" package --vulnerable --include-transitive | tee /tmp/nuget.txt + ! grep -q "has the following vulnerable packages" /tmp/nuget.txt + } + failed=0 + for p in "${blocking[@]}"; do + echo "::group::$p" + check "$p" || { echo "::error file=$p::vulnerable NuGet packages"; failed=1; } + echo "::endgroup::" + done + for p in "${report_only[@]}"; do + echo "::group::$p (report only)" + check "$p" || echo "::warning file=$p::vulnerable NuGet packages (examples are report-only)" + echo "::endgroup::" + done + exit "$failed" + + resolve: + name: Resolve lockfiles (${{ matrix.ecosystem }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ecosystem: [dart, php, ruby, python, gradle, swift] + 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 has no wrapper; ffi-ci.yml builds it with Gradle 8.9. + - if: matrix.ecosystem == 'gradle' + uses: gradle/actions/setup-gradle@v4 + with: + gradle-version: "8.9" + + - if: matrix.ecosystem == 'gradle' + name: Gradle dependency locking (antd-java, antd-kotlin, ffi/android) + 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) + + - if: matrix.ecosystem == 'swift' + uses: swift-actions/setup-swift@v2 + with: + swift-version: "6.0" + + # ffi/swift has no package dependencies (one local target generated + # at build time), so only antd-swift has anything to resolve. + - if: matrix.ecosystem == 'swift' + name: swift package resolve + working-directory: antd-swift + run: swift package resolve + + # 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: | + set -euo pipefail + git ls-files --others --ignored --exclude-standard \ + | grep -E '(^|/)(pubspec\.lock|composer\.lock|Gemfile\.lock|requirements\.txt|gradle\.lockfile|Package\.resolved)$' \ + | grep -v -E '(^|/)(\.build|build|node_modules)/' > /tmp/locks.txt || true + git ls-files --others --exclude-standard \ + | grep -E '(^|/)(requirements\.txt|gradle\.lockfile|Package\.resolved)$' >> /tmp/locks.txt || true + sort -u /tmp/locks.txt -o /tmp/locks.txt + test -s /tmp/locks.txt || { echo "::error::no lockfiles were generated"; exit 1; } + 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 + + - uses: actions/upload-artifact@v4 + with: + name: resolved-${{ matrix.ecosystem }} + path: resolved/ + if-no-files-found: error + retention-days: 7 + + osv-scanner: + name: osv-scanner (all lockfiles) + needs: resolve + 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: . + + - 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. + - 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 + 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) + 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..f0b74745 --- /dev/null +++ b/osv-scanner.toml @@ -0,0 +1,108 @@ +# 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." + +# --- 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. + +[[IgnoredVulns]] +id = "EEF-CVE-2026-43966" +ignoreUntil = 2026-12-14 +reason = "cowlib advisory via gun; unreachable through antd-elixir's client path. V2-1637." + +[[IgnoredVulns]] +id = "GHSA-w4f7-4cxr-rv3c" +ignoreUntil = 2026-12-14 +reason = "Same cowlib advisory as EEF-CVE-2026-43966 (GitHub id, reported against gun). V2-1637." From 1ee5b9e3cc317d1e1b062df01b7198dbb3c4fd3e Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 20 Oct 2026 11:27:00 +0100 Subject: [PATCH 2/5] ci(security-audit): capture osv-scanner exit codes; resolve Swift with a current toolchain First run on #382 found two problems in the workflow itself: - Actions runs `run:` steps with `bash -e`, and `set -uo pipefail` does not turn that off. The first osv-scanner call (the SARIF pass) exited 1 on findings and ended the step before the job summary, the findings table or the ::error:: line were written. The examples step, meant never to fail, died the same way. Exit codes are now captured with `|| code=$?`. - Swift resolved under Swift 6.0, which can't use SwiftNIO's security fixes (swift-nio >= 2.100.0, swift-nio-http2 >= 1.44.0, swift-nio-extras >= 1.34.1 all need swift-tools-version 6.1). So it scanned swift-nio 2.97.1, -http2 1.42.0 and -extras 1.33.0 and reported 5 advisories that a current toolchain doesn't resolve to. Swift now resolves in its own job in the official swift:6.4 image (setup-swift lags behind Swift releases). antd-swift's constraints are all `from:`, so 6.1+ resolves the fixed releases. The lockfile collection moves to .github/security-audit/collect-lockfiles.sh, shared by both resolve jobs. Refs V2-1582. Co-Authored-By: Claude Opus 5.5 --- .github/security-audit/collect-lockfiles.sh | 28 +++++++++ .github/workflows/security-audit.yml | 70 ++++++++++++--------- 2 files changed, 69 insertions(+), 29 deletions(-) create mode 100755 .github/security-audit/collect-lockfiles.sh 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/workflows/security-audit.yml b/.github/workflows/security-audit.yml index b09219b3..3fd7c5bb 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -212,7 +212,7 @@ jobs: strategy: fail-fast: false matrix: - ecosystem: [dart, php, ruby, python, gradle, swift] + ecosystem: [dart, php, ruby, python, gradle] steps: - uses: actions/checkout@v4 @@ -290,43 +290,51 @@ jobs: (cd ffi/android && gradle --no-daemon --console=plain --init-script "$INIT" resolveAndLockAll --write-locks \ -PauditConfigurations=releaseRuntimeClasspath) - - if: matrix.ecosystem == 'swift' - uses: swift-actions/setup-swift@v2 + # 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: - swift-version: "6.0" + name: resolved-${{ matrix.ecosystem }} + path: resolved/ + if-no-files-found: error + retention-days: 7 - # ffi/swift has no package dependencies (one local target generated - # at build time), so only antd-swift has anything to resolve. - - if: matrix.ecosystem == 'swift' - name: swift package resolve + # 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 package resolve + run: | + swift --version + swift package resolve - # 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: | - set -euo pipefail - git ls-files --others --ignored --exclude-standard \ - | grep -E '(^|/)(pubspec\.lock|composer\.lock|Gemfile\.lock|requirements\.txt|gradle\.lockfile|Package\.resolved)$' \ - | grep -v -E '(^|/)(\.build|build|node_modules)/' > /tmp/locks.txt || true - git ls-files --others --exclude-standard \ - | grep -E '(^|/)(requirements\.txt|gradle\.lockfile|Package\.resolved)$' >> /tmp/locks.txt || true - sort -u /tmp/locks.txt -o /tmp/locks.txt - test -s /tmp/locks.txt || { echo "::error::no lockfiles were generated"; exit 1; } - 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 + run: .github/security-audit/collect-lockfiles.sh - uses: actions/upload-artifact@v4 with: - name: resolved-${{ matrix.ecosystem }} + name: resolved-swift path: resolved/ if-no-files-found: error retention-days: 7 osv-scanner: name: osv-scanner (all lockfiles) - needs: resolve + needs: [resolve, resolve-swift] runs-on: ubuntu-latest permissions: contents: read @@ -354,14 +362,18 @@ jobs: # --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 - osv-scanner "${args[@]}" --format markdown --output-file osv.md . 2>osv.log - code=$? + 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 @@ -393,9 +405,9 @@ jobs: 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=$? + --format markdown --output-file osv-examples.md "${dirs[@]}" 2>/dev/null || code=$? { echo "## osv-scanner: examples (report only, never fails)" echo From 49b21c2ad19c218fe5ed210cf8f3ea9d1a32dff3 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 20 Oct 2026 11:47:44 +0100 Subject: [PATCH 3/5] ci(security-audit): mark the workspace safe for git in the swift container The swift:6.4 container runs as a different user from the one that owns the checked-out workspace, so git refused it ("detected dubious ownership") and collect-lockfiles.sh failed after a successful resolve. Refs V2-1582. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/security-audit.yml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index 3fd7c5bb..02408a55 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -322,8 +322,13 @@ jobs: 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: .github/security-audit/collect-lockfiles.sh + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + .github/security-audit/collect-lockfiles.sh - uses: actions/upload-artifact@v4 with: From 7dcc4b4cc1a2508723de0344929039c98e1ddcad Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 20 Oct 2026 12:29:50 +0100 Subject: [PATCH 4/5] ci(security-audit): accept basic-toml unmaintained notice; drop redundant GHSA ignore RUSTSEC-2026-0374 (basic-toml unmaintained, informational, no fix) now appears in osv's data for ffi/rust and ffi/node. basic-toml is build-time only (uniffi -> uniffi_bindgen -> askama_derive, a proc-macro), so it is not in the shipped ant-ffi library. cargo audit only warns on it. It is accepted with the same quarterly review date as the other unmaintained notices. GHSA-w4f7-4cxr-rv3c is removed: osv-scanner treats it as an alias of EEF-CVE-2026-43966, which already covers it, and reported the separate entry as unused. Refs V2-1582. Co-Authored-By: Claude Opus 5.5 --- osv-scanner.toml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/osv-scanner.toml b/osv-scanner.toml index f0b74745..8b568718 100644 --- a/osv-scanner.toml +++ b/osv-scanner.toml @@ -82,6 +82,11 @@ 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 @@ -96,13 +101,10 @@ reason = "x/crypto/openpgp is unmaintained with no fix; antd-go does not import # 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; unreachable through antd-elixir's client path. V2-1637." - -[[IgnoredVulns]] -id = "GHSA-w4f7-4cxr-rv3c" -ignoreUntil = 2026-12-14 -reason = "Same cowlib advisory as EEF-CVE-2026-43966 (GitHub id, reported against gun). V2-1637." +reason = "cowlib advisory via gun (alias GHSA-w4f7-4cxr-rv3c); unreachable through antd-elixir's client path. V2-1637." From c277b00c2326801036133bfd3b1edfbe59d0b082 Mon Sep 17 00:00:00 2001 From: Nic-dorman Date: Tue, 20 Oct 2026 13:09:40 +0100 Subject: [PATCH 5/5] ci(security-audit): fail when the NuGet audit can't run; scan ffi/kotlin; check lockfile coverage Review fixes for #382 (dirvine, both P2: ways the audit could pass without the claimed coverage): - NuGet: `check` was called as `check || ...`, and bash disables errexit inside a function called that way. A failed `dotnet restore` or `dotnet list` fell through to the final negated grep and passed as clean (reproduced: restore exit 17 -> script exit 0). The logic moves to .github/security-audit/nuget-audit.sh, which checks every exit explicitly: 0 clean, 1 vulnerable, 2 could not run. Both 1 and 2 fail for blocking projects; examples only warn. .github/security-audit/test-nuget-audit.sh runs it against a stub dotnet on every run (8 cases: clean, vulnerable, restore/list/both failing, list failing with advisories printed, two report-only cases). - ffi/kotlin: the standalone JVM UniFFI binding (JNA 5.17.0, coroutines 1.8.1, kotlin-test) is its own Gradle build, not included by any of the three that were locked, so it was never resolved or scanned. It is now locked too (36 packages locally, osv-clean), and the Gradle step fails if any of its four expected lockfiles is missing. - The osv-scanner job now checks that all 19 expected lockfiles (committed + generated) are present before scanning, listing them in the job summary, so a resolver that quietly produces less fails the run instead of leaving an SDK unscanned. The workflow header now says why ffi/python and ffi/swift have nothing to resolve: they declare no package dependencies, and their native library is ffi/rust, which is scanned. Refs V2-1582. Co-Authored-By: Claude Opus 5.5 --- .github/security-audit/nuget-audit.sh | 83 +++++++++++++++++++++ .github/security-audit/test-nuget-audit.sh | 60 +++++++++++++++ .github/workflows/security-audit.yml | 86 ++++++++++++++-------- 3 files changed, 198 insertions(+), 31 deletions(-) create mode 100755 .github/security-audit/nuget-audit.sh create mode 100755 .github/security-audit/test-nuget-audit.sh 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 index 02408a55..7e1a1d2a 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -18,7 +18,11 @@ name: Security audit # 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 -# (Java, Kotlin, Android) and Swift. +# (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 @@ -176,35 +180,13 @@ jobs: 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: | - set -euo pipefail - 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 - ) - check() { - dotnet restore "$1" >/dev/null - dotnet list "$1" package --vulnerable --include-transitive | tee /tmp/nuget.txt - ! grep -q "has the following vulnerable packages" /tmp/nuget.txt - } - failed=0 - for p in "${blocking[@]}"; do - echo "::group::$p" - check "$p" || { echo "::error file=$p::vulnerable NuGet packages"; failed=1; } - echo "::endgroup::" - done - for p in "${report_only[@]}"; do - echo "::group::$p (report only)" - check "$p" || echo "::warning file=$p::vulnerable NuGet packages (examples are report-only)" - echo "::endgroup::" - done - exit "$failed" + run: bash .github/security-audit/nuget-audit.sh resolve: name: Resolve lockfiles (${{ matrix.ecosystem }}) @@ -273,14 +255,18 @@ jobs: distribution: temurin java-version: "17" - # ffi/android has no wrapper; ffi-ci.yml builds it with Gradle 8.9. + # 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) + name: Gradle dependency locking (antd-java, antd-kotlin, ffi/android, ffi/kotlin) env: INIT: ${{ github.workspace }}/.github/security-audit/lock-all.init.gradle run: | @@ -289,6 +275,13 @@ jobs: (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. @@ -353,6 +346,37 @@ jobs: 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 }}