Skip to content

Commit ca7b99d

Browse files
author
Muhammad Ahmad
committed
Add React Native security toolkit: runtime checks, auditor, CLI and MCP server
Runtime (react-native-security-toolkit): a TurboModule with Kotlin and Swift engines covering root, jailbreak, debugger, emulator and simulator, hooking, app integrity, secure hardware, biometrics, network posture and screen capture. Every check returns signals, confidence and evidence rather than a boolean, and `unknown` is never reported as `secure`. Risk scoring is deterministic and versioned, and the policy engine returns a decision with its evidence instead of acting on it — the toolkit reports, the application decides. Auditor (@rn-security/auditor): fifteen rules over JS/TS, Kotlin, Swift, AndroidManifest, Info.plist and dependency manifests. The scanned repository is treated as hostile: nothing from it is executed, imported, evaluated or installed, including its own configuration file, which is statically evaluated. Findings carry CWE, MASVS, MASWE and MASTG identifiers generated from official OWASP and MITRE sources and validated at startup, are deduplicated by line-independent fingerprints, and support disabled rules, baselines and inline suppressions that require a reason. CLI (@rn-security/cli): audit, secrets, dependencies, runtime, report and rules commands, with console, JSON, Markdown, HTML and SARIF output. MCP (@rn-security/mcp): a read-only Model Context Protocol server, so findings can be reviewed by the model the developer already uses — no API key, no vendor and no source upload. Text quoted from the repository is labelled untrusted, and prompt injection aimed at the reviewing model is reported rather than obeyed. Also includes the example security console, intentionally vulnerable and clean fixtures, per-check and per-rule documentation, a threat model, and CI that makes the toolkit audit itself. 570 tests.
1 parent 844f869 commit ca7b99d

432 files changed

Lines changed: 80824 additions & 1 deletion

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.editorconfig

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
root = true
2+
3+
[*]
4+
charset = utf-8
5+
end_of_line = lf
6+
indent_style = space
7+
indent_size = 2
8+
insert_final_newline = true
9+
trim_trailing_whitespace = true
10+
11+
[*.{kt,kts,java}]
12+
indent_size = 4
13+
14+
[*.{swift,h,m,mm}]
15+
indent_size = 2
16+
17+
[*.md]
18+
trim_trailing_whitespace = false

.gitattributes

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.pbxproj -text
2+
# specific for windows script files
3+
*.bat text eol=crlf

.github/workflows/ci.yml

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
8+
# Least privilege by default; jobs opt in to more where they genuinely need it.
9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
16+
env:
17+
# Never let a dependency's lifecycle script run implicitly in CI.
18+
npm_config_ignore_scripts: 'true'
19+
20+
jobs:
21+
verify:
22+
name: Lint, typecheck and unit tests
23+
runs-on: ubuntu-latest
24+
# Only this job needs to publish alerts; every other job stays read-only.
25+
permissions:
26+
contents: read
27+
security-events: write
28+
steps:
29+
- name: Checkout
30+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
31+
32+
- name: Set up pnpm
33+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
34+
35+
- name: Set up Node
36+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
37+
with:
38+
node-version-file: .nvmrc
39+
cache: pnpm
40+
41+
- name: Install dependencies
42+
run: pnpm install --frozen-lockfile
43+
44+
- name: Check formatting
45+
run: pnpm format:check
46+
47+
- name: Lint
48+
run: pnpm lint
49+
50+
- name: Typecheck
51+
run: pnpm -r typecheck
52+
53+
- name: Unit tests
54+
run: pnpm -r test
55+
56+
- name: Native engine version is in sync
57+
run: node scripts/sync-native-version.mjs --check
58+
59+
- name: Package build
60+
run: pnpm build
61+
62+
# The toolkit scans its own repository (§58). Configured by
63+
# security-toolkit.config.ts, which is also the worked example of the format.
64+
- name: Security self-audit
65+
run: pnpm security:audit
66+
67+
# Generated even when the audit gate fails, so the alerts still reach code
68+
# scanning. `--min info` because GitHub does the filtering, and a finding
69+
# missing from the SARIF is a finding nobody sees.
70+
- name: Generate SARIF
71+
if: always()
72+
run: >
73+
node packages/cli/bin/rn-security.mjs audit .
74+
--format sarif --min info --out rn-security.sarif
75+
76+
- name: Upload SARIF to code scanning
77+
if: always()
78+
uses: github/codeql-action/upload-sarif@f3712979fa5f215279b101dd0a2e3bdfb4353324 # v3
79+
with:
80+
sarif_file: rn-security.sarif
81+
category: rn-security-auditor
82+
83+
build-android:
84+
name: Build Android
85+
runs-on: ubuntu-latest
86+
steps:
87+
- name: Checkout
88+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
89+
90+
- name: Set up pnpm
91+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
92+
93+
- name: Set up Node
94+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
95+
with:
96+
node-version-file: .nvmrc
97+
cache: pnpm
98+
99+
- name: Set up JDK
100+
uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
101+
with:
102+
distribution: temurin
103+
java-version: '17'
104+
105+
- name: Install dependencies
106+
run: pnpm install --frozen-lockfile
107+
108+
- name: Native unit tests
109+
working-directory: example/android
110+
run: ./gradlew :react-native-security-toolkit:testDebugUnitTest --console=plain
111+
112+
- name: Build example app
113+
working-directory: example
114+
run: pnpm build:android
115+
116+
# Google Play requires 16 KB page-size support for apps targeting Android
117+
# 15+. Our own native library is the one we can guarantee, so assert it
118+
# rather than trusting whichever NDK the runner happens to have.
119+
- name: Native library is 16 KB aligned
120+
run: |
121+
set -euo pipefail
122+
APK=$(find example/android/app/build/outputs/apk/debug -name '*.apk' | head -1)
123+
unzip -o -q "$APK" 'lib/arm64-v8a/librnsecuritytoolkit.so' -d /tmp/apkcheck
124+
READELF=$(find "$ANDROID_HOME"/ndk/*/toolchains/llvm/prebuilt/*/bin -name llvm-readelf | head -1)
125+
ALIGN=$("$READELF" -l /tmp/apkcheck/lib/arm64-v8a/librnsecuritytoolkit.so | awk '/LOAD/{print $NF; exit}')
126+
echo "LOAD alignment: $ALIGN"
127+
test "$ALIGN" = "0x4000"
128+
129+
build-ios:
130+
name: Build iOS
131+
runs-on: macos-latest
132+
steps:
133+
- name: Checkout
134+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
135+
136+
- name: Set up pnpm
137+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
138+
139+
- name: Set up Node
140+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
141+
with:
142+
node-version-file: .nvmrc
143+
cache: pnpm
144+
145+
- name: Install dependencies
146+
run: pnpm install --frozen-lockfile
147+
148+
- name: iOS engine unit tests
149+
working-directory: packages/runtime
150+
run: swift test
151+
152+
- name: Install pods
153+
working-directory: example/ios
154+
run: pod install
155+
156+
- name: Build example app
157+
working-directory: example
158+
run: pnpm build:ios

.github/workflows/release.yml

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: Release
2+
3+
# Publishing is driven by a tag, so what ships is always a commit that exists in
4+
# the repository. `workflow_dispatch` is dry-run only — a release should never be
5+
# something anyone can trigger from a button by accident.
6+
on:
7+
push:
8+
tags: ['v*']
9+
workflow_dispatch:
10+
inputs:
11+
dry_run:
12+
description: 'Run every check and pack the packages without publishing'
13+
type: boolean
14+
default: true
15+
16+
permissions:
17+
contents: read
18+
19+
env:
20+
# Never let a dependency's lifecycle script run implicitly.
21+
npm_config_ignore_scripts: 'true'
22+
23+
jobs:
24+
publish:
25+
name: Verify and publish
26+
runs-on: ubuntu-latest
27+
permissions:
28+
contents: read
29+
# Required for npm provenance: the registry verifies this workflow really
30+
# built the artefact.
31+
id-token: write
32+
33+
steps:
34+
- name: Checkout
35+
uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
36+
37+
- name: Set up pnpm
38+
uses: pnpm/action-setup@f40ffcd9367d9f12939873eb1018b921a783ffaa # v4
39+
40+
- name: Set up Node
41+
uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5
42+
with:
43+
node-version-file: .nvmrc
44+
cache: pnpm
45+
registry-url: 'https://registry.npmjs.org'
46+
47+
- name: Install dependencies
48+
run: pnpm install --frozen-lockfile
49+
50+
# The same gates as CI. A release that skips them is a release that ships
51+
# what CI would have rejected.
52+
- name: Check formatting
53+
run: pnpm format:check
54+
55+
- name: Lint
56+
run: pnpm lint
57+
58+
- name: Typecheck
59+
run: pnpm -r typecheck
60+
61+
- name: Unit tests
62+
run: pnpm -r test
63+
64+
- name: Native engine version is in sync
65+
run: node scripts/sync-native-version.mjs --check
66+
67+
- name: Build
68+
run: pnpm build
69+
70+
- name: Security self-audit
71+
run: pnpm security:audit
72+
73+
# Versions agree with each other and with the tag, every package has a
74+
# readme, a licence and a working bin. npm publishes are immutable.
75+
- name: Release preflight
76+
run: node scripts/check-release.mjs "${{ github.ref_type == 'tag' && github.ref_name || '' }}"
77+
78+
- name: Inspect what would be published
79+
run: pnpm -r --filter "./packages/*" exec npm pack --dry-run
80+
81+
- name: Publish to npm
82+
if: github.ref_type == 'tag'
83+
# `-r` publishes in dependency order and rewrites `workspace:*` ranges to
84+
# the versions actually being released.
85+
run: pnpm publish -r --filter "./packages/*" --access public --no-git-checks
86+
env:
87+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
88+
# Provenance is read from each package's publishConfig; this makes the
89+
# intent explicit at the point of publish as well.
90+
NPM_CONFIG_PROVENANCE: 'true'
91+
92+
- name: Summary
93+
if: always()
94+
run: |
95+
{
96+
echo "### Release ${{ github.ref_name }}"
97+
echo
98+
if [ "${{ github.ref_type }}" = "tag" ]; then
99+
echo "Published from tag \`${{ github.ref_name }}\`."
100+
else
101+
echo "Dry run — every check ran, nothing was published."
102+
fi
103+
} >> "$GITHUB_STEP_SUMMARY"

.gitignore

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
# Dependencies
2+
node_modules/
3+
.pnpm-store/
4+
vendor/bundle/
5+
6+
# JavaScript build output
7+
lib/
8+
dist/
9+
coverage/
10+
*.tsbuildinfo
11+
12+
# Swift Package Manager (iOS engine unit tests)
13+
.build/
14+
.swiftpm/
15+
Package.resolved
16+
17+
# React Native / Metro
18+
.metro-health-check*
19+
*.jsbundle
20+
21+
# Android build output. These patterns are intentionally unanchored: build
22+
# directories appear under example/, packages/*/android/ and app modules, and a
23+
# root-anchored pattern silently misses every one of them.
24+
build/
25+
.cxx/
26+
.gradle/
27+
.kotlin/
28+
local.properties
29+
*.iml
30+
*.hprof
31+
captures/
32+
33+
# iOS build output
34+
Pods/
35+
DerivedData/
36+
*.xcworkspace/
37+
!*.xcodeproj/project.pbxproj
38+
*.hmap
39+
*.ipa
40+
*.dSYM.zip
41+
*.xcuserstate
42+
xcuserdata/
43+
44+
# Machine-local Xcode environment: holds an absolute path to this machine's node.
45+
# React Native's template ships `.xcode.env` versioned and this one not.
46+
.xcode.env.local
47+
48+
# Generated code (Codegen output, regenerated on every build)
49+
**/generated/
50+
**/ReactCodegen/
51+
52+
# Editor / OS
53+
.DS_Store
54+
.idea/
55+
.vscode/*
56+
!.vscode/extensions.json
57+
*.swp
58+
*~
59+
60+
# Local tooling state
61+
.claude/
62+
.eslintcache
63+
.turbo/
64+
65+
# Logs
66+
*.log
67+
npm-debug.*
68+
yarn-debug.*
69+
yarn-error.*
70+
report.*.json
71+
72+
# Reports produced by this toolkit itself. Committing a scan output would put a
73+
# machine-specific snapshot in the repository and rot from the first commit.
74+
rn-security.sarif
75+
rn-security-report.*
76+
77+
# Env / secrets — never commit
78+
.env
79+
.env.*
80+
!.env.example
81+
*.pem
82+
*.p8
83+
*.p12
84+
*.keystore
85+
!debug.keystore
86+
*.mobileprovision
87+
88+
# The project brief this toolkit was built against. It is instructions for an
89+
# AI assistant working on the repository, not documentation for people using it.
90+
CLAUDE.md
91+
92+
# Re-included on purpose.
93+
#
94+
# A user-level ignore file (core.excludesFile) can exclude these by name — a
95+
# global `README.md`, `*.png` or `Podfile.lock` entry is common — and the result
96+
# is a published repository with no readme, no launcher icons and no reproducible
97+
# pod install, with nothing in the repository to explain why. A repository
98+
# .gitignore takes precedence over the global one, so these say plainly that this
99+
# project wants them.
100+
!README.md
101+
!**/README.md
102+
!*.png
103+
!**/*.png
104+
!Gemfile.lock
105+
!**/Gemfile.lock
106+
!Podfile.lock
107+
!**/Podfile.lock
108+
109+
# ...but not inside vendored dependency trees. The broad negations above would
110+
# otherwise drag in a README from every installed gem, so this re-excludes them.
111+
# Later patterns win, which is why this block is last.
112+
vendor/bundle/**
113+
**/vendor/bundle/**

0 commit comments

Comments
 (0)