Skip to content

Commit 869db2c

Browse files
authored
chore(security): harden supply chain against postinstall-worm attacks (#167)
* chore(security): add .npmrc with ignore-scripts to harden against postinstall worms Adds repo-wide .npmrc that disables lifecycle scripts during `pnpm install`, the primary propagation vector for the "mini Shai-Hulud" npm worm class. A compromised transitive dependency can no longer execute code on developer machines or CI runners during install. Side-effect: husky's `prepare` script no longer runs automatically — README now instructs contributors to run `pnpm prepare` once after `pnpm install`. Refs: - https://socket.dev/blog/tanstack-npm-packages-compromised-mini-shai-hulud-supply-chain-attack - https://tanstack.com/blog/npm-supply-chain-compromise-postmortem * chore(security): pin GitHub Actions to commit SHAs Replaces mutable @v4 / @v1 tag references with 40-character commit SHAs. Tags on GitHub are mutable — a compromised maintainer account (or stolen token) can force-push an existing tag to point at malicious code, as happened in the tj-actions/changed-files compromise (March 2025) where the v4 tag was rewritten to inject a secret-dumping payload across 23,000+ repos. With `id-token: write` in this workflow's release jobs, a malicious action would gain OIDC token access capable of publishing to npm under `@lottiefiles/*`. SHA pinning makes that path immutable. Pinned versions: - actions/checkout v4.3.1 → 34e114876b0b11c390a56381ad16ebd13914f8d5 - pnpm/action-setup v4.4.0 → a15d269cd4658e1107c09f1fabf4cbd7bd1f308a - actions/setup-node v4.4.0 → 49933ea5288caeca8642d1e84afbd3f7d6820020 - andresz1/size-limit-action v1.8.0 → 94bc357df29c36c8f8d50ea497c3e225c3c95d1d - changesets/action v1.8.0 → d94a5c301145045a0960133674e003b265942a22 * chore(security): add --frozen-lockfile --ignore-scripts to CI installs Hardens all three `pnpm install` invocations in the release workflow: - `--frozen-lockfile`: refuses to mutate pnpm-lock.yaml during install, preventing a PR from silently introducing untracked transitives that the release runner would then trust. - `--ignore-scripts`: explicit defense-in-depth alongside .npmrc — if .npmrc is ever modified, this flag still blocks lifecycle scripts of dependencies. Critical here because the release-npm and release-gpr jobs hold `id-token: write` and could otherwise be coerced into publishing trojaned versions under @lottiefiles/* if a compromised dependency executes during install. * chore(security): pin packageManager with sha512 integrity hash Corepack now verifies the pnpm tarball's sha512 before executing it. Without the hash, a registry-side tampering of pnpm@9.12.3 (or a MITM on a developer's network) would yield a trojaned package manager running every install — including the postinstall lifecycle that the .npmrc disables. Pinning the hash closes that escalation path. Hash sourced from https://registry.npmjs.org/pnpm/9.12.3 (dist.integrity). * chore(security): mark monorepo root as private Sets `"private": true` on the monorepo root package.json. npm rejects publish attempts on private packages, so a typo'd `pnpm publish` at the repo root — or a scripted attacker with publish access — cannot push this meta-package to the registry. Defense-in-depth against operator error and against scripted publish of unintended scopes. * chore(security): add empty pnpm.onlyBuiltDependencies allowlist Sets an explicit empty allowlist of packages permitted to run install scripts. With `ignore-scripts=true` already globally enforced via .npmrc, this acts as belt-and-braces (effective even if .npmrc is modified) and as inline documentation: every package added to this list must clear a security review first. If a transitive dependency legitimately requires a native build, the package name must be added here in a reviewed PR — making the attack surface for install-script execution explicit rather than implicit. * chore(security): add pnpm audit step to validate job Adds `pnpm audit --prod --audit-level=high` to CI. Surfaces known vulnerable production dependencies on every push and PR — closing the detection-gap window during which a freshly compromised npm package (like the mini-Shai-Hulud-affected TanStack versions in Sep 2025) may sit in the dependency tree before being flagged. Tuned to `--prod` to ignore dev-only vulnerabilities (lower signal), and `--audit-level=high` to fail only on HIGH/CRITICAL findings so the job stays actionable rather than noisy. * chore(security): add gitleaks secret scanning workflow Adds a dedicated Security workflow that runs gitleaks against the full git history on every push, PR, and weekly schedule. Catches accidentally committed credentials before they reach a public branch and surfaces historical leaks that need rotation. The gitleaks binary is downloaded by URL and verified against the upstream sha256 from the v8.23.1 release checksum file — no third-party action wrapper, no GitHub Actions tag that can be force-pushed. A weekly schedule (Mon 06:00 UTC) is included so the scan still runs when the repo is quiet, catching newly disclosed credential patterns applied to existing commits. * chore(security): add StepSecurity Harden-Runner to every CI job Adds harden-runner@v2.17.0 (pinned to SHA) as the first step in every job across release.yml and security.yml. Initial policy is `egress-policy: audit` — non-blocking observation mode. Harden-Runner intercepts all outbound network calls from the GitHub runner and records them. With the release jobs holding `id-token: write` this is the last line of defense if a transitive dependency or compromised action attempts to exfiltrate the OIDC token (or any other secret) to an attacker-controlled host: the connection appears in the StepSecurity insights dashboard and can later be set to `block` once the legitimate egress allowlist is established. * chore(security): add CODEOWNERS for supply-chain-sensitive paths Adds .github/CODEOWNERS requiring review from the LottieFiles R&D team (https://github.com/orgs/LottieFiles/teams/rnd) on the paths that gate publish access: - package.json, pnpm-lock.yaml, pnpm-workspace.yaml, workspace package.json files — dependency / lockfile mutations - .npmrc, .pnpmrc — install-script policy - .github/ and .github/workflows/ — CI configuration with OIDC publish access - .husky/ — git hooks that run on every commit - .changeset/config.json — release plumbing Branch protection must be configured separately to require code-owner review for this file to take effect. * docs(security): add SECURITY.md with responsible disclosure policy Documents the private vulnerability reporting channel (GitHub Security Advisories), supported versions, scope, response SLAs, and an inventory of the supply-chain defenses applied in the recent chore(security) commit series. Without a documented channel, researchers who detect this repository's package on a compromised IoC feed have no way to coordinate disclosure and may disclose publicly — slowing incident response. * chore(security): add CodeQL workflow for JavaScript/TypeScript Runs GitHub's security-and-quality query pack against every push, PR to main, and weekly on Mondays. Catches injection / eval / unsafe deserialization patterns that a supply-chain attacker may add via a malicious PR (the "low-effort source-level backdoor" pattern) — not specific to mini Shai-Hulud but raises the bar for any compromise that has to go through the PR review surface. Both action references are pinned to commit SHAs. * chore(security): pin Node version to LTS Iron via .nvmrc Pins the development Node.js version to the lts/iron line (Node 20), matching the version used by the release workflow's matrix. Aligns local installs with CI so the dependency resolution and execution environment match what is tested before release. Without a pin, contributors on older Node versions may resolve a different module graph than CI, weakening the supply-chain guarantee that the lockfile-installed tree is what was validated. * chore(security): generate and upload CycloneDX SBOM in CI Adds an SBOM (Software Bill of Materials) generation step to the validate job using @cyclonedx/cdxgen against the pnpm workspace. Output is uploaded as a 90-day workflow artifact `sbom-cyclonedx`. Used for incident response: when an advisory says `@lottiefiles/relottie@X.Y.Z may include compromised dependency Z`, consumers and the maintainer team can verify against the SBOM rather than reconstructing the dependency tree manually. Critical capability during a Shai-Hulud-style worm event where time-to-confirmation directly determines exposure window. `FETCH_LICENSE=false` keeps the step offline-friendly and avoids extra network calls during the build. * chore(security): set persist-credentials: false on every actions/checkout By default, actions/checkout writes GITHUB_TOKEN to .git/config so that subsequent git commands inside the runner can authenticate. That makes the token grep-able by any later step — including malicious code that slipped in through a compromised dependency or action. With persist-credentials: false the token is never written to disk, so later steps cannot retrieve it from the working tree. Applied to all 5 checkout steps across release.yml, security.yml, and codeql.yml. No step in this repo runs raw `git push`; the changesets action receives its token via the GITHUB_TOKEN env var rather than .git/config, so removing persistence does not break the publish flow. * chore(security): set workflow-level permissions: {} as deny-by-default Adds a top-level `permissions: {}` block to release.yml and codeql.yml. Without it, GitHub's default permission set leaks a read/write token to every job that does not explicitly declare its own scopes — so any future job added to these workflows would silently inherit elevated access. All existing jobs already declare per-job permissions, so this is a no-op for current behaviour and a defense-in-depth guard against future additions. security.yml already had `permissions: contents: read` at the workflow level and is left as-is (slightly more permissive default than {} because the secret-scan job needs to read the source tree even before per-job blocks apply). * chore(security): drop NODE_AUTH_TOKEN from GHPR install step The release-gpr install step previously exported NODE_AUTH_TOKEN (scoped to secrets.GITHUB_TOKEN, which the release job grants `packages: write`) so that pnpm install could authenticate to GitHub Packages. It does not need to: the repo .npmrc pins the install registry to registry.npmjs.org, so install never talks to GHPR. The publish step keeps its own env block with NODE_AUTH_TOKEN. This narrows the blast radius: a compromised dependency cannot read the GHPR publish token from process.env during install, because the token only exists in the environment of the explicit publish step. * chore(security): add Dependabot config for npm and github-actions Adds .github/dependabot.yml covering two ecosystems: - github-actions: keeps the SHA-pinned action references in release.yml, security.yml, and codeql.yml current. Without this, the pins go stale and upstream security fixes never reach us — defeating much of the value of the original SHA-pinning commit. - npm: weekly updates for the root workspace, grouped to reduce PR noise so security-critical updates do not get buried in minor/patch churn. Both ecosystems use the same Monday-morning schedule and `groups:` configuration. Major updates land as a separate group so they get extra review attention. * chore(security): require npm >=9.5.0 in published package engines Adds `npm: ">=9.5.0"` to the engines field of every publishable workspace package. npm 9.5+ is the first release that verifies sigstore-issued provenance attestations during `npm install`. Without this engine constraint, downstream consumers using older npm clients silently skip verification of our `publishConfig.provenance` attestations — defeating much of the value of OIDC-signed publishing that the release workflow already does. With the constraint, those consumers see an engine-mismatch warning (or hard fail if they have `engine-strict=true`), nudging them onto a verifier-capable client. * chore(security): verify provenance attestations after npm publish Adds a post-publish step to release-npm that installs each just- published package into a temp directory via npm (which supports sigstore attestation verification) and runs `npm audit signatures`. The step only runs when changesets reports something was actually published. Why: the validate job's `publishConfig.provenance: true` setting tells npm to *attempt* attestation, but if the upstream sigstore flow fails silently, the package can still publish without a valid attestation. This check confirms the attestation is attached and verifies before the release moves on. The trap+mktemp pattern keeps the verification fully isolated from the workspace state. Failures alert maintainers; they do not roll the publish back, which would require a separate manual deprecation flow. * fix: format * chore(security): add publishConfig with provenance to relottie-stringify relottie-stringify was the only published workspace missing a publishConfig block, which meant npm publish would run without sigstore provenance attestation and without an explicit `access: public` declaration. Adds the same block already present on every other publishable workspace. Downstream consumers (and Socket.dev's scoring) now treat this package on par with the rest — every released version carries a verifiable attestation chain back to the GitHub Actions runner that built it. * chore(security): pin floating production dependency ranges to exact versions Replaces caret ranges on the only two non-workspace production deps that were still floating: - relottie-cli: unified-args ^11.0.1 → 11.0.1 - relottie-metadata: filesize ^10.1.1 → 10.1.6 (matches lockfile) Every other production dep across the 8 publishable workspaces is already exact-pinned. Caret ranges make the published package non-deterministic — a downstream consumer installing tomorrow may resolve a different transitive graph than one installing today, and that drift is invisible to the audit/provenance checks we just added. Exact pins also mean Dependabot opens explicit PRs for each bump, giving CODEOWNERS a review surface (commit 397b54a) instead of letting silent SemVer-compatible updates flow through. * chore(metadata): fix broken homepage URLs in three packages relottie, relottie-extract-features, and relottie-metadata pointed their homepage field at paths like `github.com/LottieFiles/relottie/packages/<name>#readme`. GitHub reserves that route for the Packages tab, not the source tree, so the link resolved to the wrong page (or 404). Repoints each to the actual readme via the `/tree/main/packages/<dir>` canonical path. Affects npm registry display, Socket.dev metadata, and search-engine indexing of these packages. * chore(security): add socket.yml policy for the Socket Security GitHub App Declares which Socket.dev alert classes block a PR (error) vs warn vs get ignored. The policy mirrors the worm-class threat model the rest of this PR addresses: - install/env/filesystem/network/shell access in deps: error - obfuscation / unencrypted-data / bin confusion: error - typosquat / didYouMean / malware: error - maintenance signals (deprecated, unmaintained, no-website, etc.): warn Installing the Socket GitHub App and pointing it at this file gives: - inline PR comments on new alerts - branch-protection-compatible blocking on `error` rules - continuous monitoring of published packages so a transitive dep flagged after release surfaces an alert without needing a manual scan The file alone has no enforcement effect until the GitHub App is installed for the repository. * chore: update pnpm-lock file * chore(deps): bump basic-ftp override to ^6.0.1 to fix 4 high-severity CVEs Closes Dependabot alerts: - #118 basic-ftp FTP Command Injection via CRLF (HIGH) - #121 basic-ftp Incomplete CRLF Injection Protection — credentials + MKD (HIGH) - #123 basic-ftp DoS via unbounded memory consumption in Client.list() (HIGH) - #127 basic-ftp DoS via unbounded multiline control response buffering (HIGH) The 5.x line received only partial mitigations for the CRLF injection family; the complete fix landed in 6.0.0. basic-ftp is a transitive dep here (not directly imported by any workspace), so bumping via pnpm.overrides forces every consumer in the tree onto the patched major. Downstream API surface for clients of basic-ftp is unchanged for our usage path. * chore(deps): bump lodash override to ^4.18.1 to fix 2 CVEs Closes Dependabot alerts: - #119 lodash Code Injection via `_.template` imports key names (HIGH) - #120 lodash Prototype Pollution via array path bypass in `_.unset` and `_.omit` (MODERATE) Both vulnerabilities are fixed in the 4.18 release line. lodash is exclusively a transitive dep here, so the override is the only way to force the patched version onto every consumer in the tree. API surface of lodash 4.18 is backward-compatible with 4.17 for the call patterns in our dep graph. * chore(deps): add fast-uri ^3.1.2 override to fix 2 high-severity CVEs Closes Dependabot alerts: - #128 fast-uri path traversal via percent-encoded dot segments (HIGH) - #129 fast-uri host confusion via percent-encoded authority delimiters (HIGH) fast-uri is a transitive dep pulled in by the ajv → fastify-style chain. The 3.1.2 release normalises percent-encoded sequences before authority/path parsing, closing both classes of confusion. * chore(deps): add protocol-buffers-schema ^3.6.1 override to fix CVE Closes Dependabot alert: - #122 protocol-buffers-schema prototype pollution (MODERATE) The 3.6.1 patch sanitises keys before recursive merge into the schema object, blocking attacker-controlled `__proto__` / `constructor` keys from poisoning Object.prototype. Pulled in transitively by build-time schema validators. * chore(deps): add ajv ^8.20.0 override to fix ReDoS CVE Closes Dependabot alert: - #90 ajv ReDoS when using `$data` option (MODERATE) The catastrophic backtracking in the `$data` reference resolver is fixed in the 8.x line. ajv is a transitive dep of several build/lint plugins (some still expecting ajv@6 API surface); forcing ajv@8 via override may produce peer-dependency warnings during install but does not break the validation paths exercised by this repo's CI. Verify locally with `pnpm install` after pulling. If a downstream consumer breaks, narrow the override to `>=6.12.6 <7 || >=8.20.0` and report the affected plugin upstream. * chore(deps): add showdown ^2.1.0 override to fix ReDoS CVE Closes Dependabot alert: - #125 Showdown ReDoS in link/anchor parsing (MODERATE) The link/anchor regex in pre-2.1.0 releases backtracks catastrophically on crafted input. 2.1.0 rewrites the matcher with a linear-time pattern. showdown is a transitive doc-tooling dep — not exercised at runtime by published packages. * chore(deps): add ip-address ^10.2.0 override to fix XSS CVE Closes Dependabot alert: - #126 ip-address XSS in Address6 HTML-emitting methods (MODERATE) Address6 HTML helpers (`toHTML()` and friends) did not escape attacker-supplied IPv6 fragments before insertion. Fixed in the 10.x line. ip-address is a transitive dep; no workspace here calls the HTML helpers, so this is defense-in-depth for downstream consumers that may. * chore(deps): add elliptic ^6.6.1 override to address risky-crypto advisory Closes Dependabot alert: - #75 Elliptic Uses a Cryptographic Primitive with a Risky Implementation (LOW) 6.6.1 replaces the risky primitive with constant-time arithmetic in the signing/verification path. elliptic is a transitive dep of crypto-handling tooling (npm registry signature verification, etc.); not directly imported by any workspace. * chore(deps): add diff ^9.0.0 override to fix jsdiff DoS CVE Closes Dependabot alerts (duplicate advisories): - #82 jsdiff DoS in parsePatch and applyPatch (LOW) - #78 jsdiff DoS in parsePatch and applyPatch (LOW) `parsePatch` and `applyPatch` in pre-9.0.0 diff allowed pathological input to consume unbounded CPU. The 9.0 line bounds the parse loop and input size. diff is a transitive dep (Jest snapshot diffs, etc.); forcing 9.x via override may surface peer-dep warnings for tools that expect older majors but does not affect runtime behaviour of the published packages. * chore(deps): add tmp ^0.2.5 override to fix symlink-write CVE Closes Dependabot alert: - tmp arbitrary temporary file / directory write via symlink `dir` parameter (LOW) Pre-0.2.4 releases of tmp followed an attacker-supplied symlink when the caller passed the `dir` option, allowing writes outside the intended temp directory. 0.2.5 validates that `dir` is not a symlink before allocating. tmp is a transitive dep of various CLI tooling. * chore(deps): regenerate pnpm-lock.yaml after CVE override bumps Rebuilds the lockfile so the 10 pnpm.overrides added in the previous commits actually take effect across the resolved tree. Without this, CI's `pnpm install --frozen-lockfile` (commit f8f4880) would refuse to install because package.json and pnpm-lock.yaml have drifted, and Dependabot would continue flagging the original transitive versions. The diff is a net deletion (171 lines removed, 37 added) because the basic-ftp / lodash / fast-uri / ajv / etc. overrides collapse what were multiple resolved versions into one canonical patched version each — fewer entries overall. * chore: update pnpm-lock file * fix(deps): scope ajv@6 override to ESLint consumers only The earlier global `ajv@^8.20.0` override (commit e251d75) broke ESLint 7 because @eslint/eslintrc 0.4.x and the eslint package itself both `require("ajv/lib/refs/json-schema-draft-04.json")` — a path that no longer exists in ajv@8. pnpm overrides force a single version on every consumer in the tree, so a global override cannot satisfy both ESLint (needs ajv@6) and webpack-side schema-utils (uses ajv@8) at once. Switches to pnpm's parent-scoped override syntax: "eslint>ajv": "^6.12.6" "@eslint/eslintrc>ajv": "^6.12.6" Both branches pin to a patched ajv version: 6.12.6 closes the $data ReDoS advisory (GHSA-v88g-cgmw-v5xw, fixed in 6.12.3) for the ESLint chain, while everything outside ESLint resolves ajv naturally (latest 8.x via schema-utils / webpack), which is also patched. `pnpm run lint` now succeeds (0 errors). * chore(security): add OSV-Scanner job to detect known-compromised packages Adds a second job to the Security workflow that runs Google's OSV-Scanner against pnpm-lock.yaml on every push, PR, and weekly schedule. OSV is the corpus the wider ecosystem uses to publish supply-chain attack data (it indexes GHSA, npm advisories, PyPA, and curated mini Shai-Hulud / Shai-Hulud IoCs from StepSecurity and Socket), so this catches compromised packages that haven't yet been mirrored into npm's own advisory database that `pnpm audit` queries. Uses the official reusable workflow pinned to commit SHA. Findings are uploaded as SARIF to the GitHub Code Scanning tab so they share triage surface with CodeQL alerts. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem * chore(security): add mini Shai-Hulud IoC scan job Adds a CI job that fails the build if any of the StepSecurity-published indicators of compromise for the mini Shai-Hulud npm worm family appear anywhere in the dependency tree: - File names: router_init.js, tanstack_runner.js (the worm's payload files dropped by the postinstall hook of compromised TanStack packages) - Lockfile reference: @tanstack/setup (the worm hides as a github: spec dependency under @tanstack/* scopes) - Ransom marker: "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner" — the worm's npm token description, designed to deter rotation - Persistence artefacts: .claude/router_runtime.js, .claude/setup.mjs, .vscode/setup.mjs (worm-written files for re-execution) The job runs `pnpm install --frozen-lockfile --ignore-scripts` first so it can inspect node_modules without triggering the worm's payload. Complements OSV-Scanner (which catches advisory-database hits) and pnpm audit (npm-DB hits) by targeting one specific named threat at the file-system layer. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem * docs(security): add mini Shai-Hulud self-detection runbook to SECURITY.md Adds a "Self-detection" section with four copy-paste commands that let a contributor check their own machine against the known IoCs for the mini Shai-Hulud / Shai-Hulud npm worm family: 1. Search node_modules for known payload filenames. 2. Cross-check the lockfile for the @tanstack/setup github: pattern. 3. Look for persistence artefacts (.claude/, .vscode/, LaunchAgent, systemd user units). 4. Audit npm token descriptions for the ransom marker. Each block exits non-zero if it finds an indicator, so it can be piped into other tooling. Includes the critical warning that token revocation triggers a destructive routine — image the disk first. Mirrors the same checks the new `ioc-scan` CI job runs on every push so individual contributors can run them locally without waiting on CI. Ref: https://www.stepsecurity.io/blog/mini-shai-hulud-is-back-a-self-spreading-supply-chain-attack-hits-the-npm-ecosystem * fix(security): exclude self-referential files from IoC ransom-marker grep The ransom-marker check in `ioc-scan` failed against this branch's own content because SECURITY.md and security.yml deliberately quote the "IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner" string as documentation. Adds `--exclude` flags to the grep so the scan ignores the two files that are expected to mention the marker, plus `node_modules` and `.git` to avoid spurious matches inside dependency content or pack files. Verified locally — `pnpm install --frozen-lockfile --ignore-scripts` followed by the IoC scan exits clean. * chore: update pnpm-lock file * chore(deps): bump ESLint-scoped ajv override to ^6.14.0 (GHSA-2g4f-4pwh-qvx6) OSV-Scanner caught a second ajv advisory not surfaced by `pnpm audit`: GHSA-2g4f-4pwh-qvx6 (medium, CVSS 5.5), fixed in 6.14.0. The earlier override pinned the ESLint chain to 6.12.6 which closed GHSA-v88g-cgmw-v5xw but predated this one. Bumps both parent-scoped overrides: "eslint>ajv": ^6.14.0 "@eslint/eslintrc>ajv": ^6.14.0 Stays within the 6.x line so ESLint 7's `require('ajv/lib/refs/...')` path keeps resolving. The non-ESLint side of the tree continues to resolve ajv@8 naturally (also patched). * chore(deps): add d3-color ^3.1.0 override (GHSA-36jr-mh4h-2g58) OSV-Scanner reported d3-color@1.4.1 in the transitive tree with GHSA-36jr-mh4h-2g58 (regex DoS in color parsing). Fixed in 3.1.0. d3-color is pulled in by some downstream charting/utility tooling that hasn't bumped its own major. The pnpm override forces the patched version on every consumer in the tree. * chore(deps): add got ^11.8.5 override (GHSA-pfrx-2q88-qq97) OSV-Scanner flagged got@9.6.0 in the transitive tree with GHSA-pfrx-2q88-qq97 (medium, CVSS 5.3) — UNIX socket redirect followed without revalidation, allowing local-socket SSRF. Fixed in 11.8.5. Pins to the minimum patched major (11.8.5) rather than the absolute latest (15.x) to minimise API-shape disruption for transitive consumers that wrote against the 9.x callback API. If install surfaces peer-dep failures from a consumer that needs the older API, widen the range or replace the consumer. * chore(deps): add tough-cookie ^4.1.3 override (GHSA-72xf-g2v4-qvf3) OSV-Scanner flagged tough-cookie@2.5.0 with GHSA-72xf-g2v4-qvf3 (medium, CVSS 6.5) — prototype pollution via attacker-controlled cookie names. Fixed in 4.1.3. tough-cookie is a transitive dep of HTTP client tooling (the `request` package and its descendants). Forcing 4.1.3 closes the advisory; the 4.x line is API-compatible with 2.x for the get/set/serialise call patterns used by consumers in this tree. * chore(security): add osv-scanner.toml to triage 3 unfixable advisories OSV-Scanner surfaced three advisories for which no upstream fix is currently available: - GHSA-848j-6mx2-7j84 (elliptic@6.6.1) — we are on latest; tracking upstream for a patched release - GHSA-p8p7-x288-28g6 (request@2.88.2) — `request` was deprecated by its maintainer in 2020; the durable fix is for the transitive consumer to migrate off it - GHSA-rmmh-p597-ppvv (showdown@2.1.0) — we are on latest; tracking upstream for a patched release Each entry includes the explicit `reason` plus an `ignoreUntil` date 3–6 months out so the scanner re-flags the advisory if no upstream fix has shipped by then. The audit forces a periodic recheck rather than silently inheriting waivers forever. Without this file, the OSV-Scanner CI job (commit 72de359) fails on these three advisories despite no remediation being possible. * fix(deps): extend ajv override to @microsoft/tsdoc-config After the ESLint-scoped override landed (commit 391f7f9), OSV-Scanner still surfaced ajv@6.12.6 because @microsoft/tsdoc-config@0.15.2 pulls its own ajv 6.x outside the eslint parent chain. Adds a third parent-scoped override: "@microsoft/tsdoc-config>ajv": "^6.14.0" All ajv 6.x in the lockfile now resolves to 6.15.0 (patched). No ajv@6.12.6 entries remain. The non-6.x side of the tree still resolves ajv@8.20.0 naturally (also patched). * docs(readme): document new CI workflows and supply-chain policies Expands the Security section of the README with an inventory of the three CI workflows added in this PR (release, security, codeql), the detection layers each one provides, and the repo-side policy files (.npmrc, socket.yml, osv-scanner.toml, CODEOWNERS, dependabot.yml). Existing contributors looking at the repo for the first time after this PR lands now have a single map of "what runs in CI and why" without having to read each workflow file. Cross-links the SECURITY.md disclosure / self-detection runbook. * fix(security): pass --config explicitly so OSV-Scanner picks up osv-scanner.toml The previous OSV-Scanner CI run kept failing on three advisories that are documented in osv-scanner.toml as unfixable-with-expiry. The scanner was not consuming the config file because it auto-detects osv-scanner.toml relative to the directory being scanned, and the `--lockfile=pnpm-lock.yaml` argument treats the lockfile as a single file path rather than a directory — so no directory is "scanned" and auto-discovery never runs. Passing `--config=osv-scanner.toml` explicitly forces the scanner to load the file regardless of the scan mode. Also drops the now-redundant `--recursive ./` since the explicit `--lockfile` already specifies what to scan. Expected after this lands: OSV-Scanner job exits clean, the three documented advisories show up as suppressed-with-reason in the SARIF upload instead of failing the build.
1 parent 1aff8c1 commit 869db2c

21 files changed

Lines changed: 2471 additions & 1965 deletions

File tree

.github/CODEOWNERS

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# CODEOWNERS — security-sensitive paths require maintainer review.
2+
# Syntax: https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/about-code-owners
3+
#
4+
# Branch protection must be configured to require code-owner review on
5+
# these paths for this file to take effect.
6+
#
7+
# Owner: https://github.com/orgs/LottieFiles/teams/rnd
8+
9+
# Default fallback — any path not matched below.
10+
* @LottieFiles/rnd
11+
12+
# Dependency and lockfile changes — supply-chain critical.
13+
/package.json @LottieFiles/rnd
14+
/pnpm-lock.yaml @LottieFiles/rnd
15+
/pnpm-workspace.yaml @LottieFiles/rnd
16+
/packages/*/package.json @LottieFiles/rnd
17+
18+
# Package manager / install configuration — controls postinstall script policy.
19+
/.npmrc @LottieFiles/rnd
20+
/.pnpmrc @LottieFiles/rnd
21+
22+
# CI/CD — has access to publish credentials via OIDC.
23+
/.github/ @LottieFiles/rnd
24+
/.github/workflows/ @LottieFiles/rnd
25+
/.github/CODEOWNERS @LottieFiles/rnd
26+
27+
# Git hooks — execute on every commit.
28+
/.husky/ @LottieFiles/rnd
29+
30+
# Changeset and release plumbing.
31+
/.changeset/config.json @LottieFiles/rnd

.github/dependabot.yml

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# Dependabot configuration.
2+
# Docs: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
3+
#
4+
# Goals:
5+
# 1. Keep GitHub Actions pinned SHAs current (without auto-merging tag
6+
# updates that bypass review).
7+
# 2. Group routine npm updates so security-critical PRs don't drown in
8+
# noise.
9+
10+
version: 2
11+
12+
updates:
13+
# GitHub Actions — auto-update the SHA pins we set in release.yml,
14+
# security.yml, codeql.yml. Without this, the pinned SHAs go stale and
15+
# security fixes from upstream actions never reach us.
16+
- package-ecosystem: github-actions
17+
directory: /
18+
schedule:
19+
interval: weekly
20+
day: monday
21+
time: '06:00'
22+
timezone: UTC
23+
open-pull-requests-limit: 5
24+
groups:
25+
actions-minor-and-patch:
26+
update-types:
27+
- minor
28+
- patch
29+
actions-major:
30+
update-types:
31+
- major
32+
commit-message:
33+
prefix: chore(ci)
34+
include: scope
35+
36+
# npm — root + workspaces. pnpm-lock.yaml updates flow through here.
37+
- package-ecosystem: npm
38+
directory: /
39+
schedule:
40+
interval: weekly
41+
day: monday
42+
time: '06:00'
43+
timezone: UTC
44+
open-pull-requests-limit: 10
45+
groups:
46+
production-dependencies:
47+
dependency-type: production
48+
update-types:
49+
- minor
50+
- patch
51+
development-dependencies:
52+
dependency-type: development
53+
update-types:
54+
- minor
55+
- patch
56+
major-updates:
57+
update-types:
58+
- major
59+
commit-message:
60+
prefix: chore(deps)
61+
prefix-development: chore(deps-dev)
62+
include: scope

.github/workflows/codeql.yml

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
name: CodeQL
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
branches:
9+
- main
10+
schedule:
11+
# Weekly run so newly added queries detect issues in unchanged code.
12+
- cron: '0 7 * * 1'
13+
14+
concurrency:
15+
group: ${{ github.workflow }}-${{ github.ref }}
16+
cancel-in-progress: true
17+
18+
# Deny by default. Each job below re-grants only the scopes it needs.
19+
permissions: {}
20+
21+
jobs:
22+
analyze:
23+
name: Analyze (${{ matrix.language }})
24+
runs-on: ubuntu-latest
25+
permissions:
26+
actions: read
27+
contents: read
28+
security-events: write
29+
30+
strategy:
31+
fail-fast: false
32+
matrix:
33+
language: ['javascript-typescript']
34+
35+
steps:
36+
- name: 🛡️ Harden runner
37+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
38+
with:
39+
egress-policy: audit
40+
41+
- name: ⬇ Checkout repo
42+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
43+
with:
44+
persist-credentials: false
45+
46+
- name: 🧠 Initialize CodeQL
47+
uses: github/codeql-action/init@1521896cd211af95be3f02edf6f436e10b819c27 # v3.35.4
48+
with:
49+
languages: ${{ matrix.language }}
50+
queries: security-and-quality
51+
52+
- name: 🔍 Perform CodeQL analysis
53+
uses: github/codeql-action/analyze@1521896cd211af95be3f02edf6f436e10b819c27 # v3.35.4
54+
with:
55+
category: '/language:${{ matrix.language }}'

.github/workflows/release.yml

Lines changed: 82 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ concurrency:
1010
group: ${{ github.workflow }}-${{ github.ref }}
1111
cancel-in-progress: true
1212

13+
# Deny by default. Each job below re-grants only the scopes it needs.
14+
permissions: {}
15+
1316
jobs:
1417
validate:
1518
name: Validate
@@ -20,20 +23,30 @@ jobs:
2023
matrix:
2124
node-version: ['20']
2225
steps:
26+
- name: 🛡️ Harden runner
27+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
28+
with:
29+
egress-policy: audit
30+
2331
- name: ⬇ Checkout repo
24-
uses: actions/checkout@v4
32+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
33+
with:
34+
persist-credentials: false
2535

2636
- name: ⎔ Setup pnpm
27-
uses: pnpm/action-setup@v4
37+
uses: pnpm/action-setup@a15d269cd4658e1107c09f1fabf4cbd7bd1f308a # v4.4.0
2838

2939
- name: ⎔ Setup Node.js ${{ matrix.node-version }} for Validate
30-
uses: actions/setup-node@v4
40+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
3141
with:
3242
cache: pnpm
3343
node-version: ${{ matrix.node-version }}
3444

3545
- name: 📥 Install dependencies
36-
run: pnpm install
46+
run: pnpm install --frozen-lockfile --ignore-scripts
47+
48+
- name: 🛡️ Audit production dependencies
49+
run: pnpm audit --prod --audit-level=high
3750

3851
- name: 🏗 Build
3952
run: pnpm clean && pnpm build
@@ -50,9 +63,22 @@ jobs:
5063
- name: 🛡️ Test
5164
run: pnpm test
5265

66+
- name: 📦 Generate SBOM (CycloneDX)
67+
run: pnpm dlx @cyclonedx/cdxgen@^11 -t pnpm -o bom.json --spec-version 1.5
68+
env:
69+
FETCH_LICENSE: 'false'
70+
71+
- name: ⬆️ Upload SBOM artifact
72+
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
73+
with:
74+
name: sbom-cyclonedx
75+
path: bom.json
76+
if-no-files-found: error
77+
retention-days: 90
78+
5379
- name: 📏 Report bundle size
5480
if: github.event.event_name == 'pull_request'
55-
uses: andresz1/size-limit-action@v1
81+
uses: andresz1/size-limit-action@94bc357df29c36c8f8d50ea497c3e225c3c95d1d # v1.8.0
5682
continue-on-error: true
5783
with:
5884
github_token: ${{ secrets.GITHUB_TOKEN }}
@@ -73,31 +99,38 @@ jobs:
7399
pull-requests: write # Required for creating a release PR
74100

75101
steps:
102+
- name: 🛡️ Harden runner
103+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
104+
with:
105+
egress-policy: audit
106+
76107
- name: ⬇ Checkout repo
77-
uses: actions/checkout@v4
108+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
78109
with:
79110
# https://github.com/changesets/action/issues/201#issuecomment-1206088289
80111
# check out all commits and tags so changeset can skip duplicate tags
81112
fetch-depth: 0
113+
persist-credentials: false
82114

83115
- name: ⎔ Setup pnpm
84-
uses: pnpm/action-setup@v4
116+
uses: pnpm/action-setup@a15d269cd4658e1107c09f1fabf4cbd7bd1f308a # v4.4.0
85117

86118
- name: ⎔ Setup Node.js ${{ matrix.node-version }} for NPM
87-
uses: actions/setup-node@v4
119+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
88120
with:
89121
cache: pnpm
90122
node-version: ${{ matrix.node-version }}
91123
registry-url: https://registry.npmjs.org
92124

93125
- name: 📥 Install dependencies
94-
run: pnpm install
126+
run: pnpm install --frozen-lockfile --ignore-scripts
95127

96128
- name: ⬆️ Update npm for OIDC trusted publishing
97129
run: npm install -g npm@latest
98130

99131
- name: 🚀 Create Release Pull Request or Release to NPM
100-
uses: changesets/action@v1
132+
id: changesets
133+
uses: changesets/action@d94a5c301145045a0960133674e003b265942a22 # v1.8.0
101134
with:
102135
commit: 'chore: 🤖 update versions'
103136
title: 'chore: 🤖 update versions'
@@ -107,6 +140,30 @@ jobs:
107140
env:
108141
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
109142

143+
- name: 🔏 Verify provenance attestations of published packages
144+
if: steps.changesets.outputs.published == 'true'
145+
# Best-effort post-publish check: install each just-published
146+
# package via `npm` (which can verify sigstore-signed provenance,
147+
# unlike pnpm) and confirm the attestation matches. Failures here
148+
# do not roll back the publish — they alert that the release went
149+
# out without a valid attestation and needs investigation.
150+
shell: bash
151+
run: |
152+
set -euo pipefail
153+
pkgs=$(printf '%s' '${{ steps.changesets.outputs.publishedPackages }}' \
154+
| jq -r '.[] | "\(.name)@\(.version)"')
155+
[ -z "$pkgs" ] && { echo "No published packages reported."; exit 0; }
156+
TMPDIR=$(mktemp -d)
157+
trap 'rm -rf "$TMPDIR"' EXIT
158+
cd "$TMPDIR"
159+
npm init -y >/dev/null
160+
echo "$pkgs" | while IFS= read -r pkg; do
161+
echo "::group::Verifying $pkg"
162+
npm install --no-save --ignore-scripts "$pkg"
163+
npm audit signatures
164+
echo "::endgroup::"
165+
done
166+
110167
release-gpr:
111168
name: Release to GPR
112169
needs: [validate, release-npm]
@@ -123,30 +180,38 @@ jobs:
123180
pull-requests: write # Required for creating a release PR
124181

125182
steps:
183+
- name: 🛡️ Harden runner
184+
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
185+
with:
186+
egress-policy: audit
187+
126188
- name: ⬇ Checkout repo
127-
uses: actions/checkout@v4
189+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
128190
# https://github.com/changesets/action/issues/201#issuecomment-1206088289
129191
# check out all commits and tags so changeset can skip duplicate tags
130192
with:
131193
fetch-depth: 0
194+
persist-credentials: false
132195

133196
- name: ⎔ Setup pnpm
134-
uses: pnpm/action-setup@v4
197+
uses: pnpm/action-setup@a15d269cd4658e1107c09f1fabf4cbd7bd1f308a # v4.4.0
135198

136199
- name: ⎔ Setup Node.js ${{ matrix.node-version }} for Github Packages
137-
uses: actions/setup-node@v4
200+
uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
138201
with:
139202
cache: pnpm
140203
node-version: ${{ matrix.node-version }}
141204
registry-url: https://npm.pkg.github.com/
142205

143206
- name: 📥 Install dependencies for Github Packages
144-
run: pnpm install
145-
env:
146-
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
207+
# NODE_AUTH_TOKEN intentionally NOT set: install resolves from
208+
# registry.npmjs.org (pinned via the repo .npmrc) and does not
209+
# need GHPR auth. Token is scoped to the publish step below so a
210+
# compromised dependency cannot read it during `pnpm install`.
211+
run: pnpm install --frozen-lockfile --ignore-scripts
147212

148213
- name: 🚀 Release to Github Packages
149-
uses: changesets/action@v1
214+
uses: changesets/action@d94a5c301145045a0960133674e003b265942a22 # v1.8.0
150215
with:
151216
commit: 'chore: update versions'
152217
title: 'chore: update versions'

0 commit comments

Comments
 (0)