nativekeel 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -0
- package/bin/nativekeel.js +162 -0
- package/package.json +38 -0
- package/src/analyze.js +360 -0
- package/src/baseline.js +23 -0
- package/src/compare.js +55 -0
- package/src/compat.js +125 -0
- package/src/config.js +3 -0
- package/src/elf-align.js +136 -0
- package/src/interop.js +53 -0
- package/src/known-issues.js +61 -0
- package/src/native.js +454 -0
- package/src/plan.js +357 -0
- package/src/project.js +112 -0
- package/src/registry.js +89 -0
- package/src/report-html.js +87 -0
- package/src/report-md.js +42 -0
- package/src/report-sarif.js +61 -0
- package/src/report-text.js +72 -0
- package/src/rules.js +59 -0
- package/src/secrets.js +98 -0
- package/src/usage.js +127 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 NativeKeel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# NativeKeel
|
|
2
|
+
|
|
3
|
+
Health check for React Native and Expo apps. One command, no install, no account:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx nativekeel
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
It answers the questions that decide whether your app can still ship:
|
|
10
|
+
|
|
11
|
+
- **Is your React Native / Expo version still supported?** React Native supports the latest minor and the two before it.
|
|
12
|
+
- **Can you upgrade at all?** The legacy architecture was removed in 0.82. If the New Architecture is off, you are stuck at 0.81.
|
|
13
|
+
- **Which packages block the upgrade?** Native modules without New Architecture support, unmaintained or with no release in years, with known alternatives. Outdated "unmaintained" flags are cross-checked against the latest npm release.
|
|
14
|
+
- **What can you simply delete?** Packages that are installed but never imported, referenced natively or required by another package. Every one you remove is one you never upgrade.
|
|
15
|
+
- **Will the stores accept your next build?** Google Play `targetSdk` and 16 KB page size (it opens your built APK/AAB and checks the alignment of every native library), and the iOS privacy manifest.
|
|
16
|
+
- **Are secrets shipped inside your app?** AWS, Stripe, OpenAI, Anthropic, GitHub, Slack, SendGrid, Twilio, Shopify, Google OAuth keys, private keys and Supabase `service_role` keys (anon keys are fine), plus `.env` values that `react-native-config` or `EXPO_PUBLIC_` compile into the bundle. Server-side folders are reported separately.
|
|
17
|
+
- **Is transport security off?** Cleartext HTTP or `debuggable` in the release Android manifest, `NSAllowsArbitraryLoads` on iOS.
|
|
18
|
+
- **Leftovers from old templates?** Flipper, JavaScriptCore instead of Hermes.
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
✖ CRITICAL React Native 0.77.1 is no longer supported
|
|
22
|
+
Latest is 0.87.1. You are 10 minor versions behind; only the latest 3 minors receive fixes.
|
|
23
|
+
✖ CRITICAL New Architecture is disabled (android + ios)
|
|
24
|
+
The legacy architecture was removed in 0.82. This app cannot upgrade past 0.81 until it migrates.
|
|
25
|
+
✖ CRITICAL AWS secret access key shipped inside the app (src/aws.js:4)
|
|
26
|
+
Value wJal…EY. Anyone who downloads the app can extract it. Revoke it now, then move the call to a server.
|
|
27
|
+
▲ HIGH react-native-fast-image: no New Architecture support, unmaintained
|
|
28
|
+
Likely upgrade blocker. Alternatives: expo-image.
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npx nativekeel [path] # report
|
|
35
|
+
npx nativekeel plan --out UPGRADE.md # step-by-step upgrade plan
|
|
36
|
+
npx nativekeel --html report.html # shareable HTML report
|
|
37
|
+
npx nativekeel --markdown pr.md # summary for a pull request comment
|
|
38
|
+
npx nativekeel --sarif nk.sarif # GitHub code scanning
|
|
39
|
+
npx nativekeel --json # machine-readable output
|
|
40
|
+
npx nativekeel --fail-on high # exit 1 on high or critical (default: critical)
|
|
41
|
+
npx nativekeel --offline # no network at all, local checks only
|
|
42
|
+
npx nativekeel --verbose # list every outdated package
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Everything is free. The scan exits with code `1` when it finds a critical issue, so it can gate CI.
|
|
46
|
+
|
|
47
|
+
### Upgrade plan
|
|
48
|
+
|
|
49
|
+
`nativekeel plan` turns the report into ordered work: stop leaks first, replace blocking packages, switch on the New Architecture on your current version, then upgrade React Native in small, reviewable hops with links to the Upgrade Helper diffs.
|
|
50
|
+
|
|
51
|
+
### GitHub Action
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
# .github/workflows/nativekeel.yml
|
|
55
|
+
name: NativeKeel
|
|
56
|
+
on: [pull_request]
|
|
57
|
+
permissions:
|
|
58
|
+
contents: read
|
|
59
|
+
security-events: write # only needed with sarif: true
|
|
60
|
+
jobs:
|
|
61
|
+
health:
|
|
62
|
+
runs-on: ubuntu-latest
|
|
63
|
+
steps:
|
|
64
|
+
- uses: actions/checkout@v7
|
|
65
|
+
- uses: actions/setup-node@v7
|
|
66
|
+
with: { node-version: 20 }
|
|
67
|
+
- run: npm ci
|
|
68
|
+
- uses: AlicanAkyol/nativekeel@v0
|
|
69
|
+
with:
|
|
70
|
+
fail-on: critical
|
|
71
|
+
baseline: .nativekeel-baseline.json # optional
|
|
72
|
+
sarif: true # findings inline in the PR
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The report is added to the job summary. With a baseline, only findings that are new since the baseline fail the build:
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
npx nativekeel --save-baseline .nativekeel-baseline.json # commit this file
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Reports for clients
|
|
82
|
+
|
|
83
|
+
Agencies can put their own name on the HTML report: `npx nativekeel plan --html plan.html --brand "Acme Mobile"`.
|
|
84
|
+
|
|
85
|
+
## Why you can run this on private code
|
|
86
|
+
|
|
87
|
+
- **Your code never leaves your machine.** The only requests are package-name lookups on npm and React Native Directory. `--offline` makes none; a test traps `fetch` to prove it.
|
|
88
|
+
- **Zero dependencies, no install scripts.** Only Node.js built-ins.
|
|
89
|
+
- **Read-only.** It never changes your project.
|
|
90
|
+
- **Secrets are always masked** in every output.
|
|
91
|
+
- **Verifiable releases.** Published with npm provenance from GitHub Actions: `npm audit signatures`.
|
|
92
|
+
|
|
93
|
+
Details and vulnerability reporting: [SECURITY.md](SECURITY.md).
|
|
94
|
+
|
|
95
|
+
## Need it done for you?
|
|
96
|
+
|
|
97
|
+
We do fixed-price React Native and Expo upgrades on a branch of your repository, delivered as a pull request with test builds. No calls needed. See https://nativekeel.com/#services.
|
|
98
|
+
|
|
99
|
+
## License
|
|
100
|
+
|
|
101
|
+
MIT
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { loadProject } from '../src/project.js';
|
|
6
|
+
import { analyze, SEVERITIES } from '../src/analyze.js';
|
|
7
|
+
import { textReport } from '../src/report-text.js';
|
|
8
|
+
import { htmlReport } from '../src/report-html.js';
|
|
9
|
+
import { sarifReport } from '../src/report-sarif.js';
|
|
10
|
+
import { markdownReport } from '../src/report-md.js';
|
|
11
|
+
import { buildPlan, planToMarkdown } from '../src/plan.js';
|
|
12
|
+
import { applyBaseline, saveBaseline } from '../src/baseline.js';
|
|
13
|
+
import { compareScans, compareToMarkdown } from '../src/compare.js';
|
|
14
|
+
import { SITE_URL } from '../src/config.js';
|
|
15
|
+
|
|
16
|
+
const VALUE_FLAGS = new Set(['--html', '--out', '--baseline', '--save-baseline', '--brand', '--sarif', '--markdown', '--fail-on']);
|
|
17
|
+
const COMMANDS = new Set(['scan', 'plan', 'compare', 'help']);
|
|
18
|
+
|
|
19
|
+
function parseArgs(argv) {
|
|
20
|
+
const flags = {};
|
|
21
|
+
const positional = [];
|
|
22
|
+
for (let i = 0; i < argv.length; i++) {
|
|
23
|
+
const a = argv[i];
|
|
24
|
+
if (VALUE_FLAGS.has(a)) {
|
|
25
|
+
if (!argv[i + 1] || argv[i + 1].startsWith('--')) fail(`${a} needs a value`);
|
|
26
|
+
flags[a.slice(2)] = argv[++i];
|
|
27
|
+
} else if (a.startsWith('--') || a === '-h' || a === '-v') {
|
|
28
|
+
flags[a.replace(/^-+/, '')] = true;
|
|
29
|
+
} else {
|
|
30
|
+
positional.push(a);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const command = COMMANDS.has(positional[0]) ? positional.shift() : 'scan';
|
|
34
|
+
return { command, flags, positional };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fail(message, code = 2) {
|
|
38
|
+
console.error(`nativekeel: ${message}`);
|
|
39
|
+
process.exit(code);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const HELP = `NativeKeel: health check for React Native and Expo apps
|
|
43
|
+
|
|
44
|
+
Usage
|
|
45
|
+
npx nativekeel [path] Scan an app and print the report
|
|
46
|
+
npx nativekeel plan [path] Write a step-by-step upgrade plan
|
|
47
|
+
npx nativekeel compare <before.json> <after.json>
|
|
48
|
+
What got fixed and what is new between two scans
|
|
49
|
+
|
|
50
|
+
Scan options
|
|
51
|
+
--json Machine-readable output
|
|
52
|
+
--html <file> Save a shareable HTML report
|
|
53
|
+
--markdown <file> Save a Markdown summary (for pull request comments)
|
|
54
|
+
--sarif <file> Save SARIF for GitHub code scanning
|
|
55
|
+
--verbose List every outdated package
|
|
56
|
+
--offline Make no network requests (local checks only)
|
|
57
|
+
--save-baseline <file> Accept current findings as known
|
|
58
|
+
--baseline <file> Only report findings not in the baseline (for CI)
|
|
59
|
+
--brand <name> Put your company name on the HTML report
|
|
60
|
+
--fail-on <level> Exit 1 at this severity or above: critical (default),
|
|
61
|
+
high, medium, low, or never
|
|
62
|
+
|
|
63
|
+
Plan options
|
|
64
|
+
--out <file> Save the plan as Markdown (default: print it)
|
|
65
|
+
--html <file> Save report + plan as one HTML file
|
|
66
|
+
|
|
67
|
+
The scan exits with code 1 when a critical issue is found (see --fail-on), so it
|
|
68
|
+
can gate CI.
|
|
69
|
+
|
|
70
|
+
Privacy: your code never leaves your machine. Without --offline, the only
|
|
71
|
+
requests are package-name lookups on registry.npmjs.org and
|
|
72
|
+
reactnative.directory. Secret values are always masked. ${SITE_URL}`;
|
|
73
|
+
|
|
74
|
+
async function runScan(root, flags) {
|
|
75
|
+
let project;
|
|
76
|
+
try {
|
|
77
|
+
project = loadProject(root);
|
|
78
|
+
} catch (e) {
|
|
79
|
+
fail(e.message);
|
|
80
|
+
}
|
|
81
|
+
if (!flags.json) process.stderr.write(`Scanning ${root}${flags.offline ? ' (offline)' : ''} …\n`);
|
|
82
|
+
return analyze(project, flags.offline ? { get: async () => null, offline: true } : {});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function packageVersion() {
|
|
86
|
+
return JSON.parse(fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '..', 'package.json'), 'utf8')).version;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function write(file, content) {
|
|
90
|
+
fs.writeFileSync(file, content);
|
|
91
|
+
process.stderr.write(`Saved ${path.resolve(file)}\n`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function main() {
|
|
95
|
+
const { command, flags, positional } = parseArgs(process.argv.slice(2));
|
|
96
|
+
|
|
97
|
+
if (flags.v || flags.version) {
|
|
98
|
+
console.log(packageVersion());
|
|
99
|
+
return 0;
|
|
100
|
+
}
|
|
101
|
+
if (command === 'help' || flags.h || flags.help) {
|
|
102
|
+
console.log(HELP);
|
|
103
|
+
return 0;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (command === 'compare') {
|
|
107
|
+
if (positional.length !== 2) fail('usage: nativekeel compare <before.json> <after.json> (scans saved with --json)');
|
|
108
|
+
const [before, after] = positional.map((f) => {
|
|
109
|
+
try {
|
|
110
|
+
return JSON.parse(fs.readFileSync(f, 'utf8'));
|
|
111
|
+
} catch (e) {
|
|
112
|
+
return fail(`cannot read ${f}: ${e.message}`);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const md = compareToMarkdown(compareScans(before, after));
|
|
116
|
+
if (flags.out) write(flags.out, md);
|
|
117
|
+
else console.log(md);
|
|
118
|
+
return 0;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const root = path.resolve(positional[0] || '.');
|
|
122
|
+
const brand = flags.brand || null;
|
|
123
|
+
|
|
124
|
+
if (command === 'plan') {
|
|
125
|
+
const result = await runScan(root, flags);
|
|
126
|
+
const plan = buildPlan(result);
|
|
127
|
+
if (flags.html) write(flags.html, htmlReport(result, { plan, brand }));
|
|
128
|
+
const md = planToMarkdown(result, plan);
|
|
129
|
+
if (flags.out) write(flags.out, md);
|
|
130
|
+
else if (!flags.html) console.log(md);
|
|
131
|
+
return 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const failOn = flags['fail-on'] || 'critical';
|
|
135
|
+
if (!['critical', 'high', 'medium', 'low', 'never'].includes(failOn)) fail('--fail-on must be one of critical, high, medium, low, never');
|
|
136
|
+
let result = await runScan(root, flags);
|
|
137
|
+
if (flags['save-baseline']) {
|
|
138
|
+
const count = saveBaseline(flags['save-baseline'], result);
|
|
139
|
+
process.stderr.write(`Baseline saved with ${count} known findings: ${path.resolve(flags['save-baseline'])}\n`);
|
|
140
|
+
}
|
|
141
|
+
if (flags.baseline) {
|
|
142
|
+
try {
|
|
143
|
+
result = applyBaseline(flags.baseline, result);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
fail(`cannot read baseline ${flags.baseline}: ${e.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (flags.html) write(flags.html, htmlReport(result, { brand }));
|
|
150
|
+
if (flags.markdown) write(flags.markdown, markdownReport(result, { verbose: !!flags.verbose }));
|
|
151
|
+
if (flags.sarif) write(flags.sarif, `${JSON.stringify(sarifReport(result, { version: packageVersion() }), null, 2)}\n`);
|
|
152
|
+
if (flags.json) console.log(JSON.stringify(result, null, 2));
|
|
153
|
+
else console.log(textReport(result, { color: process.stdout.isTTY, verbose: !!flags.verbose }));
|
|
154
|
+
if (failOn === 'never') return 0;
|
|
155
|
+
const threshold = SEVERITIES.indexOf(failOn);
|
|
156
|
+
return result.findings.some((f) => SEVERITIES.indexOf(f.severity) <= threshold) ? 1 : 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
main().then(
|
|
160
|
+
(code) => process.exit(code),
|
|
161
|
+
(e) => fail(e.stack || e.message),
|
|
162
|
+
);
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nativekeel",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Health check for React Native apps: version support, New Architecture blockers, unmaintained native modules and leaked secrets.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nativekeel": "bin/nativekeel.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"react-native",
|
|
18
|
+
"expo",
|
|
19
|
+
"upgrade",
|
|
20
|
+
"new-architecture",
|
|
21
|
+
"doctor",
|
|
22
|
+
"health-check",
|
|
23
|
+
"security"
|
|
24
|
+
],
|
|
25
|
+
"license": "MIT",
|
|
26
|
+
"scripts": {
|
|
27
|
+
"test": "node --test test/*.test.js",
|
|
28
|
+
"scan": "node bin/nativekeel.js"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://nativekeel.com",
|
|
31
|
+
"repository": {
|
|
32
|
+
"type": "git",
|
|
33
|
+
"url": "git+https://github.com/AlicanAkyol/nativekeel.git"
|
|
34
|
+
},
|
|
35
|
+
"bugs": {
|
|
36
|
+
"url": "https://github.com/AlicanAkyol/nativekeel/issues"
|
|
37
|
+
}
|
|
38
|
+
}
|