All notable changes to this project are documented here.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Nothing yet.
0.1.0 - 2026-08-19
First release. Pre-1.0: the runtime checks have not yet been validated on physical rooted or jailbroken hardware, and the public API may change in a minor release. See docs/runtime/validation.md.
-
Release tooling: a tagged
Releaseworkflow that re-runs every CI gate, checks the tag against the package versions, inspects what would be published, and publishes all four packages with npm provenance;pnpm release:checkas a local preflight; andpnpm release:dry-runto rehearse the whole thing without publishing. -
A README and a LICENSE in every published package — npm shows both on the package page, and a package without them looks abandoned before anyone reads a line of it.
-
docs/release.md, including an honest status for each of the project's acceptance criteria and why this is 0.1.0 rather than 1.0. -
Opt-in Android package visibility via
rnsecPackageVisibility=truein the consuming app'sgradle.properties, which selects a library manifest carrying<queries>for known root-manager packages. Off by default:<queries>merge into the consumer's manifest and appear in store review, so enabling them belongs to the application author.QUERY_ALL_PACKAGESis never used. -
Project foundation: pnpm workspace, strict TypeScript, ESLint, Prettier, Jest and a hardened GitHub Actions workflow.
-
Public TypeScript API surface:
SecurityToolkit,SecurityToolkitError,isSecurityToolkitError, and the structured result model (SecurityCheckResult,SecuritySignal,SecurityStatus,SecurityConfidence,UnavailableReason). -
Codegen TurboModule bridge with Kotlin (Android) and Objective-C++/Swift (iOS) engine skeletons, each running work off the JavaScript thread.
-
SecurityToolkit.getEngineInfo()as an installation smoke test. -
Runtime validation of every payload crossing the native boundary.
-
Configuration with validation, per-call native timeouts, and an example app.
-
Example app: theme-aware Lottie animations reflecting native engine state (example-only dependency; the published package still declares
dependencies: {}). -
docs/architecture/build-and-workspace.mdrecording the workspace, Gradle, CocoaPods, Metro and Babel decisions and the failure modes behind them. -
Android root detection (
RootDetection.getStatus()): ten independent signals covering su binaries, root management applications, build and boot-state properties, mount anomalies, writable protected directories, SELinux state, and Magisk/Zygisk artefacts, behind a versioned signature pack. -
Android security engine: injectable probe layer, detector registry, signal aggregator and bridge mapper, with 53 Kotlin unit tests that run without a rooted device.
-
JNI probe for Android system properties via
__system_property_get, replacing approaches that either silently fail (System.getProperty) or rely on restricted non-SDK interfaces. -
SignalOutcome(detected/not-detected/indeterminate) on every signal, so a blocked probe is distinguishable from a clean result. -
Android debugger detection (
DebuggerDetection.getStatus()/isAttached()): JDWP attachment,TracerPid, debuggable-build, developer-options-enabled and ADB-debugging-enabled signals. -
Android emulator detection (
EmulatorDetection.getStatus()/isEmulator()): build identity, emulator-only device nodes and properties, and hardware profile, against a signature pack that covers modernranchuand Cuttlefish images rather than only the QEMU/goldfish generation. -
Android hook and instrumentation detection (
HookDetection.getStatus()): dynamic instrumentation agents and injected threads, managed-code hooking frameworks, and native symbol redirection detected viadlsym/dladdrrather than prologue-byte matching. -
Android application integrity (
IntegrityCheck.getStatus()): signing-certificate pinning, install source, package identity and APK location. Unconfigured signals reportindeterminaterather than passing, so a check that was never performed cannot look like one that passed. -
integrityconfiguration onSecurityToolkit.configure, validated up front — a truncated or non-hex fingerprint is rejected rather than making every launch look like a tampered build. -
Android secure hardware capability (
SecureHardware.getStatus()): key storage backing determined by generating and deleting a throwaway Keystore key, StrongBox availability, and hardware-backed key attestation availability. -
Android biometric capability (
BiometricSecurity.getStatus()): Class 3 biometric usability, enrolment state, and device credential presence, using the platformBiometricManagerso the package adds no dependency to consuming applications. -
Android network posture (
NetworkSecurity.getStatus()): cleartext traffic policy, proxy configuration, VPN transport, and user-added certificate authorities. Proxy and VPN are weighted as informational because both are mainstream, and the proxy host is never collected. -
Android screen capture protection (
ScreenSecurity.getStatus()/enableProtection()/disableProtection()):FLAG_SECUREapplied and re-applied across activity lifecycle events so it survives rotation and activity recreation. -
Runtime detector documentation covering signals, confidence, false positives, false negatives, limitations and recommended application response: root, debugger, emulator, hooks, integrity, secure hardware, biometrics, network, screen.
-
iOS security engine: Foundation-only Swift core with injected probes, published as a Swift Package so it can be unit-tested with
swift teston macOS — no simulator and no jailbroken device. Real probe implementations live outside the package target and are compiled into the pod. -
iOS jailbreak detection (
JailbreakDetection.getStatus()): classic and rootless filesystem artefacts as separate signals, a correctly oriented sandbox escape probe, injected libraries,DYLD_INSERT_LIBRARIES, opt-in package-manager URL schemes, and symbolic-link anomalies. -
iOS debugger detection: kernel
P_TRACEDflag via publicsysctl, and an unexpected-parent heuristic that declines to answer on the simulator rather than firing on every development run.ptrace(PT_DENY_ATTACH)is deliberately not used — non-public API, App Review risk, and a mitigation rather than a detection. -
iOS simulator detection (
SimulatorDetection.getStatus()/isSimulator()), the one check in the toolkit with no inconclusive case. -
iOS hook and instrumentation detection: native symbol origin via
dlsym/dladdr, and Objective-C method implementations checked against the framework that should provide them. -
iOS application integrity: bundle identity, embedded provisioning profile, and main-binary FairPlay encryption read from
LC_ENCRYPTION_INFO_64. App Attest remains a separate optional adapter, as Play Integrity does on Android. -
integrity.expectedBundleIdentifierconfiguration for iOS. -
iOS secure hardware capability: Secure Enclave key creation (attempted, not inferred from a flag) and keychain usability.
-
iOS biometric capability:
LAContextavailability with the specific reason it is unusable, biometry type, and device passcode presence. No biometric data is read or exposed. -
iOS network posture: App Transport Security arbitrary-loads policy, proxy configuration, and VPN-style interface presence — the last two weighted as informational, since
utunon iOS is also used by Personal Hotspot, AirPlay, content filters and Private Relay. -
iOS screen capture detection via
UIScreen.isCaptured. iOS has no public API to prevent a screenshot, soenableProtection()resolves tofalsethere rather than claiming success. -
docs/runtime/secure-hardware-ios.md,docs/runtime/biometrics-ios.md,docs/runtime/network-security-ios.md. -
docs/runtime/hook-detection-ios.md,docs/runtime/integrity-ios.md,docs/runtime/jailbreak-detection.md,docs/runtime/debugger-detection-ios.md,docs/runtime/simulator-detection.md. -
Deterministic, versioned risk engine (
rnsec-risk-1): weighted signal scoring with confidence multipliers, capped uncertainty penalties for inconclusive checks, mitigation credits awarded only for checks that completed cleanly, and a full contributor list on every result — a bare score is never emitted. -
Security policy engine (
SecurityToolkit.evaluate): blocking rules per check, a risk-level threshold, capability requirements, and aminimumConfidencefloor so weak detections are recorded without denying. The decision is returned and nothing is enforced. -
SecurityToolkit.checkAll(): runs every check the platform implements in a single bridge crossing, omitting checks the platform does not implement rather than reporting them as errors. -
docs/runtime/risk-scoring.md,docs/runtime/security-policy.md. -
Static security auditor (
@rn-security/auditor,packages/auditor): the scanning engine — hostile-repository-safe file discovery, language and file-role classification, JavaScript/ TypeScript/JSX/TSX parsing with a content-keyed parse cache, the rule contract, line-number-free fingerprints, deduplication, three-layer suppression, contextual severity resolution and configuration loading. A separate package so that an application installing the runtime never installs a parser. Rules themselves land in the next phase;builtinRulesis intentionally empty. -
Auditor configuration (
security-toolkit.config.{ts,mts,cts,js,mjs,cjs,json}) is parsed and statically evaluated, never imported. Literals, template strings without substitutions, spreads,as constand one level of top-levelconstreference are supported; anything dynamic is refused by naming what was rejected and where. -
Suppression at three layers — disabled rules, a fingerprint baseline, and inline
security-audit-ignore RULE-ID reason="..."directives in any comment syntax. A directive without a reason does not suppress: it is reported as a suppression error, so a malformed suppression shows the finding rather than swallowing it. -
Adversarial test suite for the auditor: symbolic links pointing outside the project and back into it, symbolic-link loops, files that are enormous or binary or both, pathological nesting, unicode and awkward path names, a manifest with install scripts, a configuration file with side effects, and prompt injection in source. The assertions are mostly about what the auditor did not do.
-
docs/auditor/architecture.mdanddocs/auditor/configuration.md, including the auditor's known limitations: a single hostile file can stall a scan because parsing cannot be interrupted, and only the JavaScript family is parsed so far. -
Fourteen security rules covering secrets, insecure storage, broken cryptography, predictable randomness, cleartext traffic, disabled TLS validation, WebView configuration, deep links, sensitive logging, AndroidManifest configuration, exported components, iOS App Transport Security, dependency resolution and dynamic code execution. Each has a documentation page in docs/rules and four classes of test — true positive, true negative, edge case, and the false positive it would most plausibly produce.
-
Versioned security knowledge layer: CWE, MASWE, MASVS and MASTG identifiers generated from the official OWASP and MITRE sources by
pnpm --filter @rn-security/auditor knowledge:sync, pinned to upstream commits and committed so scans work offline and reproduce. Snapshot2026.1carries 24 MASVS controls, 78 MASWE weaknesses, 181 MASTG tests and 101 CWE entries (catalogue 4.20). -
A fabricated standards identifier now fails at rule registration. Rules carry identifiers only; titles are resolved from the snapshot, and
knowledge.mastgTestsFor()answers "how do I verify the fix?" with a real MASTG test rather than a paragraph of advice. Mapping confidence is declared per rule, so an arguable mapping is published as arguable. -
pnpm security:audit— the toolkit scans its own repository (§58), configured bysecurity-toolkit.config.tsat the root, which doubles as the worked example of the configuration format. The repository currently reports no findings. -
Fixture projects under
fixtures/:vulnerable-react-nativeexists to make every rule fire, andsecure-react-nativeis the same application written safely, where every finding would be a false positive. Both are scanned end to end by the test suite. -
Analysis helpers shared by rules: a bounded AST walker, sensitive-name heuristics with an explicit benign-compound list, entropy shaping that ignores UUIDs, hashes and placeholders, and an XML and property-list scanner that never resolves an entity — so a hostile manifest has nothing to expand.
-
Five report formats — console, JSON, Markdown, HTML and SARIF 2.1.0 — rendered from one scan, so a CI job that publishes SARIF and prints a summary is describing the same run. Every format states when a scan was incomplete, keeps the local project root out unless asked, and escapes content that came from the repository under analysis.
-
SARIF output is validated in CI against the specification's own schema, committed unmodified under
packages/auditor/src/reporting/__tests__/fixtures/. It carriessecurity-severityand asecuritytag so GitHub code scanning grades alerts correctly, the line-number-free fingerprint as apartialFingerprintso an alert survives edits above it, suppressed findings as dismissed results with their recorded reason, and an incomplete scan as a tool notification. -
CI generates and uploads SARIF to GitHub code scanning (
github/codeql-action/upload-sarif, pinned to a commit SHA, withsecurity-events: writescoped to that one job). -
node packages/auditor/scripts/self-audit.mjs [path] --format <name> [--min <severity>] [--out FILE]renders any format from a real scan. The full CLI is Phase 9. -
@rn-security/cli— thern-securitycommand:audit,secrets,dependencies,runtime,reportandrules, with--format,--out,--fail-on,--min,--configand colour control. Argument parsing uses Node's ownparseArgs, so the CLI adds no dependency beyond the auditor itself. -
Fixed exit codes, so a pipeline can tell events apart:
0nothing met the threshold,1a finding met--fail-on,2usage or configuration error,3a bug in the tool. Rule errors and an incomplete scan are reported loudly but never fail a run — conflating a flaky timeout with a vulnerability is how a gate stops being trusted. -
rn-security runtimereports project readiness, and says plainly that it cannot check a device: it looks for the declarations that decide whether a runtime signal returns a verdict or an honestunknown(USE_BIOMETRIC,ACCESS_NETWORK_STATE,networkSecurityConfig,LSApplicationQueriesSchemes,NSFaceIDUsageDescription, and integrity configuration). -
rn-security reportre-renders a saved JSON report in another format, so a CI job publishes SARIF and a Markdown summary from one scan rather than two. -
rn-security rules --format jsonincludes the MASTG test identifiers that verify each rule's weakness, so "how do I check the fix?" has a machine-readable answer. -
CI now gates on
pnpm security:audit, which runsrn-security audit . --fail-on highagainst this repository, and generates its SARIF through the same CLI. -
@rn-security/mcp— a Model Context Protocol server exposing the toolkit's findings to whichever AI model the developer already uses. No API key, no vendor, no source upload: it speaks stdio to a client on the same machine, so the model is one the developer already chose and already trusts. Four read-only tools:security_audit,security_rules,security_rule_detailsandsecurity_runtime_readiness, each finding carrying severity, confidence, evidence, remediation, CWE/MASVS/MASWE references with official titles, and the MASTG tests that verify a fix. -
rn-security mcp [path]runs that server from the CLI, and explains how to install it when the optional package is absent. -
MCP
2025-06-18implemented directly — one file, no dependencies. The official SDK brings seventeen transitive packages (express, hono, jose, ajv, zod) for HTTP transports and OAuth this server does not use. -
Path confinement: the model chooses every tool argument, so every path is resolved and confined to the root the server started in.
../../etcand/etcare refused. Without it, "audit this project" is an arbitrary-file-read tool. -
Untrusted-content labelling: every payload names the fields quoted from the scanned repository and tells the client they are data, and the server's
initializeinstructions say the same. AI is non-authoritative by construction here — every finding comes from a deterministic rule, and the model cannot change a severity. -
RNSEC-AI-001— a rule for prompt injection aimed at an AI code reviewer: text in the repository telling a model to ignore its instructions, report the code as safe, disclose its prompt or exfiltrate secrets. Reported and quoted verbatim, never rewritten. Mapped to CWE-1427 at low confidence, because that weakness describes an application prompting an LLM and this is a repository addressing someone else's. -
docs/security/threat-model.md— assets, threat actors, trust boundaries, and an explicit list of what the toolkit does not defend against. -
docs/security/dependencies.md— every dependency justified, and the ones that were rejected with the reason. A consumer installing only the runtime package downloads nothing; the auditor pulls five packages, all Babel. -
docs/runtime/validation.md— what has actually been verified and what has not. Physical-device validation for both platforms is outstanding, and the README says so rather than implying otherwise. -
docs/runtime/README.md— an index of the runtime checks. -
Measured performance for the static auditor: 2,000 files in 0.53 s, 10,000 in 2.0 s, and 20,500 in 4.5 s with the file cap correctly reported as truncation. Scaling is linear and peak heap stays under 106 MB.
-
The README is rewritten for people evaluating the toolkit rather than for people building it: what it does, what it refuses to claim, install, usage for all three parts, coverage, and an honest status table. Internal phase tracking and specification cross-references are gone.
-
.gitignorerewritten. Build output was matched by root-anchored patterns that missedexample/android/**/build,packages/*/android/build,.cxx,Podsand vendored gems entirely — a first commit would have published 30,467 files instead of 411. It now also re-includesREADME.md,*.png,Gemfile.lockandPodfile.lock, which a user-level global ignore was excluding: without that, the published repository would have had no readme, no launcher icons and no reproducible pod install, with nothing in the repository to explain why. -
CLAUDE.mdis no longer published. It is the project brief written for an AI assistant working on this repository, not documentation for people using the toolkit. -
Runtime readiness analysis moved from the CLI into the auditor, where both the CLI and the MCP server can use one implementation.
-
ai.enabled: truenow explains that this package runs no model and points at the MCP server, rather than saying the feature is "not available in this version" — it is available, and it works the other way round. -
The auditor's
self-audit.mjsscript is replaced by the CLI. It existed so the capability was real before the CLI landed; keeping both would have meant two implementations of the same pipeline. -
The auditor exports its XML and property-list scanners, sensitivity heuristics and entropy helpers, so a consumer building its own checks has the primitives the built-in rules use.
-
The native specification exposes generic
runCheck/runChecksrather than one method per check, so each platform's engine declares its own support instead of JavaScript hardcoding which checks exist where. -
Check configuration is passed per call rather than stored natively, keeping the native engine stateless: there is no ordering hazard between configuring and checking.
-
UnavailableReasongainssimulator, shared across the TypeScript, Kotlin and Swift layers.
-
The CLI reported a hardcoded version, which would have drifted from
package.jsonat the first release and been recorded in every JSON and SARIF report it wrote. It now reads the version from its own manifest. -
RNSEC-AI-001matched ordinary prose about scanners — "a rule handed an empty tree would report the file as clean" is a comment in this package's own parser, and the rule reported it on its first run against this repository. It now matches whole comment blocks rather than single lines, so a wrapped sentence is read as the description it is. -
Auditor false positives found by scanning this repository and the secure fixture, each now covered by a test: an Android intent filter inheriting the schemes of a filter declared later in the manifest; a log message that merely mentioned a sensitive word being treated as logging one; a kebab-case constant (
security-audit-ignore) scoring high enough on entropy to look like a credential; licence and documentation URLs inside comments reported as cleartext endpoints; and prose mentioningsecurity-audit-ignorebeing parsed as a malformed suppression directive. -
Swift package build output (
.build/) is excluded by default. Without it a scan of this repository discovered 2,681 files instead of 300, most of them compiler module caches. -
Rules that describe code no longer run on documentation: a page explaining that MD5 is broken contains the string
MD5, and reporting it reports the warning rather than the weakness. Secret detection still scans documentation, because a credential pasted into a README is a shipped credential — the rule's own page suppresses its illustrative key inline, with a reason. -
SARIF artifact URIs are percent-encoded. A repository can contain a file named
src/<img src=x onerror=alert(1)>.ts, which is a legal POSIX filename and not a legal URI; emitted raw it produced a SARIF file that failed schema validation, and a rejected upload is silent from the developer's side. Found by the hostile-content fixture. -
Package visibility was being inferred and the inference was wrong.
RNSEC-ANDROID-ROOT-002asked at runtime whether any other package was visible, but Android always exposes a handful of system packages regardless of<queries>— so the check reported "no root management application detected" on applications that had declared nothing. A false negative presented as a clean result. It is now a build-time fact (BuildConfig.PACKAGE_VISIBILITY_DECLARED) set by the same Gradle flag that selects the manifest, so the two cannot disagree. -
checkAll()scales its native timeout by the number of checks rather than borrowing the single-check budget. On an Android cold start nine sequential checks exceeded the 5 s default and the whole aggregate reportedNATIVE_TIMEOUTinstead of a result. -
RNSEC-ANDROID-ROOT-004now matchesdev-keysas well astest-keys, and no longer describes a clean result as "signed with release keys" — a claim it had not verified. Emulator and engineering images are commonlydev-keys, and were being reported as release-signed. -
Biometric signals now explain why a status was unavailable — most usefully, that the application has not declared
USE_BIOMETRIC— rather than reporting an unactionable "could not be determined". -
iOS hook detection matches system image paths by prefix rather than substring. A rootless jailbreak installs to
/var/jb/usr/lib/, which contains/usr/lib/; substring matching classified every injected library on a modern jailbroken device as a system image, blinding the check to precisely where iOS hooks live. -
Jailbreak detection reports
unavailablewith reasonsimulatorrather than a verdict when run on the iOS Simulator, and its path list excludes paths that exist on macOS (/bin/bash,/bin/sh, the ssh binaries). Both were making the check fire on every simulator run — the kind of false alarm that teaches developers to ignore a signal. -
Native library is explicitly linked with 16 KB page alignment. NDK r27 does not align by default, which made the app fall back to page-size compatibility mode on Android 15+ devices.
-
The Android NDK version is pinned rather than defaulting to the highest installed on the build machine, which was neither reproducible nor reliably complete.
- The native module is resolved with
TurboModuleRegistry.getrather thangetEnforcing, so importing the package cannot crash a host application when the native side is not linked. - Native payloads are validated at runtime rather than trusted from their Codegen types, on the premise that a compromised device may control the native side.
- CI pins every third-party action to a full commit SHA and runs with least-privilege permissions.
- Check results are rejected if they contradict themselves — in particular a
secureverdict whose own signals report a detection, which is the direction a tampered native module would lie in. - A probe that cannot run produces
indeterminate, and anyindeterminatesignal downgrades the whole check tounknown. Absence of evidence is never reported as evidence of absence.
- No security check is implemented yet.
supportedChecksis intentionally empty.