make check-security runs every enabled scanner and prints one verdict. Each
make check-security-<job> runs one scanner and the same verdict, so a single
scanner on a laptop still ends in the pass/fail answer CI would give.
The scanners are not in this repository. They are shared-workflows' own,
shipped in @blinkbitcoin/app-tooling and run here as pnpm exec check-security [job]. What each one reads, where the findings and the verdict
go, every setting with its environment twin, the LLM jobs and the known limits
of the scanners are on the shared page:
security.md in shared-workflows.
This page holds only what is this app's own choice.
| File | What it holds |
|---|---|
security-settings.json |
The settings: which jobs run, the severity threshold, the Android permission and bundle allowlists. Every key and its default are in the package's own file |
rules/ |
The Semgrep rule for this app's storage wrapper, run after the package's React Native rules ( jobs.code.rules) |
osv-scanner.toml |
Accepted dependency advisories, each with its reasoning |
.mobsf |
Accepted mobsfscan findings |
.gitleaks.toml |
Allowlisted test data for the secret scan |
.github/zizmor.yml |
Accepted workflow findings |
In CI the same scanners run through the reusable check-security.yml, at the
same commit as the package: the security job in ci.yml on every change and
the one in cd-production.yml before any store job on action=release.
SECURITY_ENABLED=false as a repository variable turns both off. The two LLM
reviews are off in security-settings.json until you set llm.provider and a
key.
Each scanner reads its own configuration, and every suppression carries a
reason: osv-scanner.toml for advisories, .semgrepignore and rules/ for
source patterns, .mobsf for mobsfscan, .gitleaks.toml for secrets,
.github/zizmor.yml for workflows, and the allowlists under jobs.bundle and
jobs.binaries in security-settings.json for the bundle and binary checks.
An entry with no reason is not mergeable. Never raise the severity threshold
to hide one finding: that hides the next one too.
These osv-scanner advisories are suppressed in osv-scanner.toml today, each
with no clean upgrade available. The two image-size ids beside them are
explained with auditConfig.ignoreGhsas in pnpm-workspace.yaml.
uuid@7.0.3(GHSA-w5hq-g745-h8pq, CVSS 7.5 high) - pulled in throughexpo>@expo/config-plugins>xcode@3.0.1, which hard-pins the dead 7.x line. Unreachable: the only call isuuid.v4()duringexpo prebuild, never in the shipped app, andv4()is not even an affected function.decode-uri-component@0.2.2(GHSA-vcc3-ghjq-m6fr, CVSS 6.6 medium) - pulled in throughexpo-router>query-string@7.1.3, which cannot take the ESM-only 0.5.0 fix without breaking its ownrequire()call. Unlike the entry above, this one is reachable at runtime throughquery-string.parse()on an incoming deep link - a denial-of-service surface the owner has knowingly accepted, not one ruled out as unreachable.node-forge@1.4.0(GHSA-86w9-cpqp-85rv, high) - pulled in throughexpo>@expo/cli, directly and through@expo/code-signing-certificates. No fixed release exists. Unreachable: only the Expo CLI loads it, on a developer machine or CI runner, and it verifies only signatures it made itself or certificates the developer configured; the shipped app checks update signatures in native code.braces@3.0.3(GHSA-vfj7-8cjw-p6xm, CVSS 7.5 high) - pulled in throughmicromatch@4.0.8, whose dependents are Metro's file map, Jest, GraphQL codegen, the Lingui CLI andfast-glob. No fixed release exists. Unreachable from the app: a deeply nested brace pattern only makes a build, test or codegen process exit, on a developer machine or CI runner, over patterns from the repository's own configuration.@graphql-tools/utils@11.2.2(GHSA-7mx3-vvmw-hjmv, high) - declared as^11bygraphql-yoga(the mock API), the GraphQL codegen packages and@graphql-tools/url-loader'sdelegateandwrap, all devDependencies. The 11.x line has no fixed release; the 12.x copies are on the fixed 12.0.3. Unreachable: the onlymergeDeepcaller is@graphql-tools/delegate, underurl-loader, which codegen never uses (its schema is a local file), and the mock API stitches nothing.sprintf-js@1.0.3(GHSA-hp3w-g68c-fv3c, CVSS 5.3 medium) - pulled in through Jest coverage's@istanbuljs/load-nyc-config>js-yaml@3>argparse@1. No fixed release exists. Unreachable: only js-yaml's own command-line binary loadsargparse, nothing here runs it, andargparseformats only its own templates.
- mobsfscan
android_task_hijacking1andandroid_task_hijacking2(StrandHogg 1.0 and 2.0, CWE-1021) onMainActivity, suppressed in.mobsf. Expo generates the activity withlaunchMode="singleTask", which expo-router's deep links rely on. The mitigation,taskAffinity=""through a config plugin, is a native change of its own and not made yet; until it lands this is a known, accepted risk. - The Android debug source sets (
android/app/src/debug*) are left out of the mobsfscan run: they allow Metro's cleartext localhost connection and are never part of a release variant.
See osv-scanner.toml for the full reasoning on each advisory: the dependency path,
why no upgrade is possible, the reachability verdict, and what upstream
change would require re-evaluating the decision.
Unlike a Semgrep or gitleaks suppression, an osv-scanner ignore leaves no
trace in run output: osv-scanner drops an IgnoredVulns match from its
SARIF entirely rather than emitting it with a suppression marker, so
the verdict has nothing to count and make check-security-dependencies simply
reports dependencies: clean - the summary line's suppressed count stays 0
regardless. osv-scanner.toml and this section are therefore the only
places these accepted risks are visible; do not read a clean dependencies
run, on its own, as nothing being carried.
rn-secret-in-storage-wrapper (rules/app-storage-wrapper.yaml) reports a
credential-named key written through this template's storage.set wrapper in
src/lib/storage.ts, which sits in front of expo-sqlite/kv-store. The
package's rn-secret-in-plain-storage rule covers AsyncStorage and
kv-store directly, so the app's own writes would be missed without this one.
It shares that rule's two deliberate gaps: no unqualified key identifier, and
no fused-lowercase authtoken.
rules/ needs its own exclusion in every tool that discovers files by a
generic convention. rules/app-storage-wrapper.test.tsx is deliberately
uninstantiable snippet code in Semgrep's <rule-id>.test.tsx fixture
convention. Four tools exclude rules/ for that reason, each with a comment:
tsconfig.json's exclude, biome.json's files.includes (!rules),
eslint.config.mjs's globalIgnores and jest.config.ts's
testPathIgnorePatterns. All four are anchored to the repository root, so an
adopter's own src/rules/ stays linted and tested. A new tool that walks the
repository by convention needs the same one-line, root-anchored entry.
make check-security here, with every deterministic scanner installed and
APK pointing at a release's universal APK, reports no finding at or above
high, so nothing blocks. The findings below that line:
dependencies,policy,bundle,sbom: clean (the accepted advisories above are filtered out).code: Semgrep findings, the highest medium (mutable GitHub Actions tags, service-account strings in docs paths, the minimum-release-age policy pattern).mobile: mobsfscan findings, the highest medium:allowBackupon the main manifest, plus the hardening notes mobsfscan always raises.binaries: three mediumMASTG-TEST-0254findings, the release manifest'sSYSTEM_ALERT_WINDOW,READ_EXTERNAL_STORAGEandWRITE_EXTERNAL_STORAGE. None is allowlisted inbinaries.androidPermissions: each needs a reason there or removal from the manifest, and that decision is the app owner's.
A finding that turns out to be acceptable gets a reasoned entry in its own scanner's configuration, as above, never a raised threshold.