119 opengrep rules that detect supply-chain and toolchain attacks against React Native developers on macOS — malicious install scripts, repo-controlled editor/agent config, invisible-Unicode payloads, unpinned CI actions, signing-material leaks, and more, across the whole polyglot RN toolchain (npm/Yarn/pnpm, Ruby/CocoaPods, Gradle, Xcode, git, CI).
Point it at a React Native repository — your own, or one someone just handed you — and it turns an unbounded "read everything" review into a bounded "read these forty lines" one.
This repository is a companion tool, not a standalone project. Every rule here is a codified version of a detection command from The React Native Developer's Security Guide — specifically, the rg/grep-based "HOW YOU CATCH IT" block that follows every attack example in the book, and the checklist in the book's Chapter 29 ("Detection: what to grep for"). Nothing here was invented independently of the book; each rule's message explains the attacker technique it targets, and each rule's metadata.book-reference field names the exact chapter that tells the full story — the real incident, the primary source, and why the check works.
If a rule fires and you want the context, the book chapter it cites is where that context lives. This repo is deliberately the part of the book you can point a tool at, not a replacement for reading it.
The React Native Developer's Security Guide is a field guide about the other half of React Native security — not the shipped app and its end users (secure storage, cert pinning, OWASP MASVS, all covered exhaustively elsewhere), but the developer, the workstation, the toolchain, the repository, and the CI system that is an extension of all four. Its argument, in one sentence: an RN repository is a polyglot execution environment disguised as a JavaScript project, and git clone && yarn && pod install && yarn ios runs untrusted, attacker-controllable code in five different runtimes before a line of the app executes.
The book is organized into ten parts and 33 chapters — foundations, the Node/JS layer, config files that are secretly programs, the Apple side, the Android side, the perimeter and the human, what happens on macOS after a compromise, defense, and a chapter on a cross-platform surface (malicious assets) that doesn't fit neatly into "Apple" or "Android" alone. If you haven't read it, the single highest-value page to start with is Appendix A — The File Risk Atlas, a one-page reference table of every file in an RN repo that can execute code and when — it's the mental model this entire rule set is organized around.
rn-dev-security-guide-opengrep/
├── README.md this file
├── LICENSE MIT
├── deep-scan.semgrepignore opt-in override to scan node_modules/ too
└── 01-install-scripts.yaml one file per book chapter (or chapter cluster),
02-lockfile-integrity.yaml numbered to roughly track reading order —
03-js-config-execution.yaml see the table below for the exact mapping
...
20-malicious-assets.yaml
Every YAML file is a small, self-contained rules: list in standard opengrep/semgrep syntax, with a header comment explaining the file's theme and citing the book chapter(s) it covers. There's no build step, no dependencies to install beyond opengrep itself, and no hidden state — clone the repo and point opengrep at it.
# Install opengrep (see https://github.com/opengrep/opengrep for other platforms,
# including a Windows PowerShell installer and prebuilt release binaries):
curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
# Clone this repo once:
git clone https://github.com/stephanww/rn-dev-security-guide-opengrep.git
# From the root of the RN repo you want to screen:
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .That's it for a first pass. Everything below is refinement: how to read the output, how to scan your dependency tree too, how to wire it into CI, and where the edges of what a static scanner can do are (they are real edges — see "What this cannot do").
A finding is a reason to look, not a verdict. Plenty of legitimate React Native projects have install hooks, husky hooks, ENV[...] reads in a Podfile, and custom Metro transformers. The value of these rules is converting an unbounded "read everything" review into a bounded "read these forty lines" review — not producing a clean/dirty verdict.
Two fields in every rule's metadata help you triage:
confidence—HIGHmeans the pattern is specific enough that a match is rarely benign (a scheme pre-action in a default RN project, a published IOC, an invisible Unicode character).LOWmeans the rule is intentionally broad and exists to build an inventory for you to read (e.g. "here is every custom Metro transformer hook in this repo").category—security(read this now),best-practice(a hardening control that's absent), orinventory(informational, not a claim of risk).
Severity (ERROR / WARNING / INFO) tracks the same idea and is what most CI integrations key off of.
cd /path/to/repo-under-audit
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .Add --json for machine-readable output, --severity ERROR to see only the high-signal stuff first, or -q to suppress the banner.
opengrep's scan command only takes local files/folders as its target — there's no opengrep scan https://github.com/.... (--config does accept a git+<url> for fetching a remote rule set, which is a different thing — that's not what you want here.) Clone it first, then scan the checkout:
git clone https://github.com/org/repo.git
cd repo
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .That's fine for a repo you already trust or are just curious about. If it's a repo from someone you don't fully trust — a recruiter, a bug reporter, a client's first handoff — don't just cd in and scan it: several things in the book's Appendix A execute before you've read a line (submodule hooks during --recurse-submodules, .vscode/tasks.json on folder-open, etc.). Follow Appendix B — the safe-clone runbook instead:
git --version # >= 2.45.1 (CVE-2024-32002)
git -c core.symlinks=false clone --no-checkout <URL> repo && cd repo
git show HEAD:package.json | jq '.scripts' # peek at the dangerous files
git show HEAD:.vscode/tasks.json 2>/dev/null # before anything is on disk
git checkout HEAD # now materialize the tree
opengrep scan --config /path/to/rn-dev-security-guide-opengrep .Run the scan itself after git checkout HEAD but before you open the folder in an editor or run any install command.
opengrep, like semgrep, ignores node_modules/ by default — always, regardless of any rule's paths.include. That default is correct for everyday scanning. But a handful of rules in this set are specifically designed to audit your dependency tree the way the book's Chapter 5 describes: after an --ignore-scripts install, scan node_modules for transitive install hooks, obfuscation, and invisible Unicode before anything in it has run.
To include node_modules in the scan, pass the bundled deep-scan.semgrepignore (an absolute path is required — opengrep will error on a relative one):
# 1. Fetch the tree WITHOUT executing it (this is the step that matters):
yarn install --immutable --mode=skip-build
# or: pnpm install --frozen-lockfile --ignore-scripts
# or: npm ci --ignore-scripts
# 2. Now scan it, including node_modules:
opengrep scan --config /path/to/rn-dev-security-guide-opengrep \
--experimental \
--semgrepignore-filename "$(cd /path/to/rn-dev-security-guide-opengrep && pwd)/deep-scan.semgrepignore" \
.This is slower (there are a lot of files in node_modules) and noisier (rules like rn-security-install-hook-present will also fire on every dependency that has a routine node-gyp rebuild build step) — that's expected. It's the same trade-off the book makes in Chapter 5: this is a bounded review of the real dependency graph, not a clean/dirty signal.
Every file's header comment cites the exact book chapter(s) it's drawn from. This table is the map; open the file for the full rationale on any individual rule.
| File | Book chapter(s) | What it covers |
|---|---|---|
01-install-scripts.yaml |
Ch 5, Ch 8 | npm/yarn/pnpm lifecycle hooks in your package.json and (in deep-scan mode) transitive dependencies; install hooks that invoke an AI agent CLI (the nx/s1ngularity technique) |
02-lockfile-integrity.yaml |
Ch 9 | overrides/resolutions redirecting a dependency; non-registry resolved hosts in package-lock.json; patches/*.patch adding execution primitives |
03-js-config-execution.yaml |
Ch 10 | metro.config.js, babel.config.js, app.config.js, eslint.config.*, jest.config.js etc. containing execution primitives, dynamic URL requires, custom Metro transformer/serializer hooks |
04-package-manager-plumbing.yaml |
Ch 11 | .yarnrc.yml yarnPath/plugins/registry redirection; .npmrc script-shell/_authToken/ignore-scripts=false; pnpm-workspace.yaml dangerouslyAllowAllBuilds/minimumReleaseAge; .pnpmfile.cjs presence; NODE_OPTIONS preloading; .envrc presence |
05-git-attack-surface.yaml |
Ch 12 | .gitattributes filters, .gitmodules submodules, .lfsconfig, husky hook execution primitives, core.hooksPath redirection |
06-editor-agent-config.yaml |
Ch 13 | .vscode/tasks.json folderOpen auto-execution, .vscode/settings.json repo-controlled interpreter paths, .devcontainer initializeCommand, MCP server declarations |
07-ruby-ios.yaml |
Ch 14, Ch 17 | Gemfile/Fastfile/Appfile execution primitives, non-RubyGems sources, sudo gem/bundle/pod, known-compromised fastlane plugins/gems, hardcoded MATCH_PASSWORD/P12_PASSWORD |
08-cocoapods.yaml |
Ch 15 | Podfile execution primitives, :git => pod sources, podspec prepare_command/script_phase, Podfile.lock checksum tracking |
09-xcode-build.yaml |
Ch 16 | Xcode scheme pre-actions (near-zero-FP, a clean RN project has none), project.pbxproj shell-script obfuscation and count tracking, Package.swift execution, react-native-xcode.sh integrity |
10-signing-material.yaml |
Ch 17, Ch 24 | osascript/keychain-touching code outside your own interactive shell, embedded PEM private keys, EAS Update/CodePush signing mentions, .p12 imported into login.keychain instead of a temporary keychain, create-keychain with no matching delete-keychain |
11-gradle-config.yaml |
Ch 18 | build.gradle/settings.gradle execution primitives (fires at sync, not just build), apply from: <url>, insecure repo URLs, buildSrc, ~/.gradle/init.d references |
12-gradle-wrapper.yaml |
Ch 19 | gradle-wrapper.properties distributionUrl/missing checksum, suspicious primitives in gradlew, missing wrapper-validation CI step |
13-android-secrets.yaml |
Ch 20 | Keystore passwords in gradle.properties/local.properties, keystore file references, google-services.json server keys, Play service-account JSON, .gitignore gaps |
14-dev-server-binding.yaml |
Ch 21 | Metro/RN CLI scripts missing --host 127.0.0.1 (CVE-2025-11953 context), cli-server-api version inventory |
15-ci-workflows.yaml |
Ch 23 | Actions not pinned to a commit SHA, tj-actions/reviewdog IOCs, pull_request_target + fork checkout, workflow_run + artifact consumption, missing permissions:, self-hosted runners |
16-invisible-unicode.yaml |
Ch 7, Ch 13, Ch 29 | The single highest-value check in this set. Zero-width/bidi characters anywhere (near-zero FP); variation-selector runs (the GlassWorm steganography signature); both, with a dedicated rule for AI agent instruction files |
17-obfuscation.yaml |
Ch 22, Ch 29 | eval/Function/atob combined with hex-mangled identifiers, large base64 blobs, very long lines, a generic env-var-read + outbound-request combination |
18-known-iocs.yaml |
Ch 5, Ch 6, Ch 7, Ch 8, Ch 14, Ch 15, Ch 24 | Published indicators from named, dated 2025–2026 incidents: the March 2026 RN package compromise (package names, C2 IP, Solana wallet), Shai-Hulud, chalk/debug, SleeperGem, s1ngularity, AMOS staging artifacts |
19-hardening-config-present.yaml |
Ch 27, Ch 31 | Positive assertions — flags the absence of ignore-scripts, min-release-age/minimumReleaseAge/npmMinimalAgeGate, a hash-pinned packageManager, and CODEOWNERS coverage of executable-config paths. This is the literal "CI check that your hardening is still present" the book recommends, so a control removed under deadline pressure fails loudly instead of silently. |
20-malicious-assets.yaml |
Ch 32 | SVG files containing <script>/event handlers/javascript: URIs; build scripts shelling out to ImageMagick or JS image-processing libraries with their own CVE history |
opengrep scan --config /path/to/rn-dev-security-guide-opengrep/16-invisible-unicode.yaml .Zero-width characters, bidi text-direction overrides, and Unicode variation selectors do not render. GlassWorm (a self-propagating OpenVSX/VS Code extension worm, October 2025) and the "Rules File Backdoor" (hidden instructions to AI coding agents, disclosed March 2025) both defeat human code review this way — a reviewer looking directly at the malicious line sees blank space, in the editor and in the GitHub PR diff. No human reviewer can catch this by reading the screen; only byte-level tooling can. It's one file, it runs in well under a second, and it closes an entire attack class. Put it in CI first, before anything else here.
# .github/workflows/security.yml
name: security-checks
on: [push, pull_request]
jobs:
opengrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Checkout rule set
uses: actions/checkout@v4
with:
repository: stephanww/rn-dev-security-guide-opengrep
path: opengrep-rules
- name: Install opengrep
run: curl -fsSL https://raw.githubusercontent.com/opengrep/opengrep/main/install.sh | bash
- name: Run rn-security rules
run: opengrep scan --config opengrep-rules --error --severity ERROR .--error makes opengrep exit non-zero on any finding at or above the given --severity, which fails the build. Starting with --severity ERROR only (the high-confidence rules — invisible Unicode, scheme pre-actions, known IOCs, .npmrc token leaks, pull_request_target + fork checkout, etc.) keeps the initial rollout noise-free; add WARNING once the team is used to triaging findings. See 19-hardening-config-present.yaml for the config-presence checks the book's Chapter 31 recommends specifically so a hardening setting that gets removed under deadline pressure is a visible, named CI failure rather than a silent regression six months later.
If you want this to run before yarn/npm install, wire it as a pre-commit or pre-push hook instead — but see the caveat in Appendix A about .husky/* itself being repo-controlled: keep your CI copy of this rule set independent of what a compromised repo could edit. Pinning the actions/checkout step above to a commit SHA rather than a branch avoids exactly the mutable-reference problem the book's Chapter 23 describes.
Static text/AST matching has real limits, and the book is explicit about them (Chapter 29, "What detection will not do"). Specifically:
- It will not catch a competent, targeted attacker. Bespoke malware written specifically for you has no signature. Detection is the third line of defense — prevention (book Ch 27) and isolation (book Ch 28) are where the real leverage is, and no rule file substitutes for running an unknown repository in a disposable VM — the one piece of advice in the book its author calls non-negotiable.
- It cannot hash-verify binaries.
gradle-wrapper.jaris a compiled JAR; opengrep can check the surrounding.propertiesfile and flag a missing checksum, but the actual JAR integrity check isgradle/actions/wrapper-validation@v4in CI (mentioned in12-gradle-wrapper.yaml's info-level rule) or a manualshasum -a 256comparison. - It cannot check machine state. Persistence (
~/Library/LaunchAgents, shell rc files,~/.gradle/init.d), keychain contents, listening ports, and login items are outside a source-tree scanner's reach entirely. Run the book's Appendix C — macOS triage one-liners or thepreflight.shscript from its Chapter 29 for that layer, and keep your copy of that script outside the repo you're auditing. - It does not replace
npm audit/osv-scanner/ gitleaks / TruffleHog. Those find known CVEs and known-pattern secrets respectively; this rule set finds structural and behavioral risk (execution primitives in files that shouldn't have them, redirection points, published incident IOCs). Run all of them — the book's Chapter 29 has the full toolbox list.
Every rule was validated with opengrep validate (0 fatal errors across all 119 rules) and then run against a purpose-built fixture tree reproducing the book's own defanged attack examples (a malicious postinstall, a metro.config.js with a disguised credential-recon function, a Podfile post_install block, a scheme pre-action, invisible Unicode in an agent-rules file, and so on) to confirm true positives fire.
The rules were then run against four real, unrelated production React Native codebases and iterated until the obvious noise was gone. This mattered — the first pass produced several false-positive patterns that only show up against real code, not synthetic fixtures, and the fix for each is recorded in that rule's message so you understand why something isn't flagged, not just that it isn't:
- Emoji variation selectors (
console.log('⚠️ ...')) matched the invisible-Unicode check on nearly every real codebase tested. Fixed by requiring a run of 2+ consecutive variation selectors — the actual GlassWorm steganographic signature — instead of a single one, which is normal emoji rendering. nc\sinside "sync" (a bare, unanchored substring match for thenccommand) matched the word "sync" in a completely unrelated comment inside a stock CocoaPods build phase. Fixed with a word boundary.:path =>inios/Podfilematched on every single React Native native-module link (pod 'X', :path => '../node_modules/...'is standard autolinking boilerplate), drowning out the real signal (:git =>, which actually bypasses the registry). Split into two rules.require($URL)as a semgrep pattern matched anyrequire()call with any argument, not just URLs — a metavariable with no constraint matches anything. Fixed withmetavariable-regex.ENV[...]in a Podfile/Fastfile/Gemfile is extremely common and legitimate (ENV['USE_FRAMEWORKS'],ENV['CI']) — downgraded to a separate low-severity inventory rule, kept distinct from the high-signalsystem/backtick/IO.popenexecution primitives.evalinandroid/gradlew— the official, unmodified Gradle wrapper script legitimately containsevalas part of its argument-quoting logic. Removed from the "suspicious primitive" pattern; the network/exfiltration primitives (curl,wget,base64,osascript, etc.) remain.- Base64 blobs and very-long-lines fired constantly on SVG assets and mock fixture data (base64-encoded sample images in test fixtures). Excluded
**/*.svg,**/mock/**,**/fixtures/**, and test files from both checks. - A handful of rules stayed intentionally noisy — e.g.
rn-security-keystore-password-in-repoandrn-security-google-services-server-keyfound real plaintext keystore passwords and API keys across the test codebases. That is the rule working, not a bug to fix.
If a rule fires on something you're confident is benign in your codebase, prefer adding a path exclusion in that rule's paths.exclude (with a comment explaining why) over lowering its severity or deleting it — the next person's fork of your config should get the same protection you tuned in.
Each rule file is a small, self-contained rules: list — normal opengrep/semgrep YAML. To add a rule:
- Find (or create) the file matching the book chapter it belongs to.
- Follow the existing pattern:
idprefixedrn-security-, amessagethat explains the attacker technique (not just "this looks risky"), abook-referenceinmetadatalinking back to the specific chapter, and aconfidence/categorypair. - Validate:
opengrep validate <your-file>.yaml - Test it against something that should trip it and something that shouldn't, the way the book's Chapter 29 insists on — "test your detection by running it against something you know is bad… and if you never test it, you have a green check mark and no idea what it means."
If the rule reflects a gap or correction in the book itself — a new technique, a false positive, a case the book's narrative should have covered — open the issue against the book repo too. The two projects move together: a narrative gap gets fixed in the chapter first, then the rule follows, per the book README's "how reader feedback becomes book content" section.
- The React Native Developer's Security Guide — the companion book this rule set is extracted from
- opengrep documentation
- Book Chapter 29 — Detection: what to grep for — the source material for most of this rule set, including the
preflight.shscript this rule set complements - Book Appendix A — The file risk atlas — the reference table this rule set is organized around
- Book Appendix C — macOS triage one-liners — the machine-state checks a source-tree scanner cannot do
MIT. The companion book is licensed separately — see its repository for details (Creative Commons Attribution 4.0 International).