reaproof 0.3.0 → 0.4.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/README.md +51 -19
- package/package.json +1 -1
- package/skills/reaproof/SKILL.md +10 -6
- package/src/bytes.mjs +44 -2
- package/src/checks/paths.mjs +38 -2
- package/src/checks/secrets.mjs +124 -18
- package/src/cli.mjs +89 -34
- package/src/electron.mjs +16 -41
- package/src/macho.mjs +256 -0
- package/src/macos.mjs +110 -28
- package/src/program.mjs +26 -0
- package/src/remote.mjs +58 -32
- package/src/report.mjs +242 -58
- package/src/rules.mjs +25 -9
- package/src/scan.mjs +244 -78
package/src/report.mjs
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { isFixable } from './fix.mjs';
|
|
2
|
-
import { groupByRule, SOURCE_MAP_RULES, STANDALONE_MAP_RULES, worstSeverity } from './rules.mjs';
|
|
2
|
+
import { groupByRule, LEAK_SEVERITIES, SOURCE_MAP_RULES, STANDALONE_MAP_RULES, worstSeverity } from './rules.mjs';
|
|
3
3
|
import { toDisplayPath } from './scan.mjs';
|
|
4
4
|
|
|
5
|
-
const LEAK_SEVERITIES = ['critical', 'high', 'medium'];
|
|
6
5
|
const MAX_LISTED_PATHS = 5;
|
|
7
6
|
const MAX_LISTED_ADDRESSES = 10;
|
|
8
7
|
const INDENT = ' ';
|
|
@@ -12,28 +11,43 @@ const KO = {
|
|
|
12
11
|
severity: { critical: '심각', high: '높음', medium: '보통', info: '안내' },
|
|
13
12
|
severityName: { critical: '심각', high: '높음', medium: '보통' },
|
|
14
13
|
target: (paths) => `대상: ${paths}`,
|
|
14
|
+
pickedByName: '(package.json 옆에서 이름으로 고른 결과물 폴더)',
|
|
15
15
|
scanned: (count) => `읽어 본 파일 ${count}개`,
|
|
16
|
+
ofSeen: (count) => `만난 파일 ${count}개 중`,
|
|
17
|
+
programsAmong: (count) => `실행 파일 ${count}개 포함`,
|
|
16
18
|
leaksLabel: '찾은 누수',
|
|
17
19
|
noLeaks: '없음',
|
|
18
20
|
leakKinds: (count) => `${count}종류`,
|
|
21
|
+
// By exit code, the ones the help below lists.
|
|
22
|
+
closings: {
|
|
23
|
+
1: (level, name) => `종료 코드 1: ${name} 이상의 누수가 있어 멈춥니다 (--fail-on ${level}).`,
|
|
24
|
+
3: () => '종료 코드 3: 판정하지 못한 채 끝납니다.',
|
|
25
|
+
},
|
|
19
26
|
riskLabel: 'rea 위험도',
|
|
20
|
-
riskLevels: { 'very-high': '매우 높음', high: '높음', moderate: '보통', baseline: '기본' },
|
|
27
|
+
riskLevels: { 'very-high': '매우 높음', high: '높음', moderate: '보통', baseline: '기본', unrated: '판정 없음' },
|
|
21
28
|
riskSummaries: {
|
|
22
29
|
'very-high': 'rea 같은 도구로 원본 코드나 비밀 값을 그대로 가져갈 수 있습니다.',
|
|
23
|
-
high: '비밀에 가까운 값이나 데이터, 실행 중인
|
|
24
|
-
moderate: '
|
|
30
|
+
high: '비밀에 가까운 값이나 데이터, 또는 실행 중인 프로그램의 속이 드러나 있습니다.',
|
|
31
|
+
moderate: '비밀 값은 나오지 않았지만, 아래 항목이 코드를 읽거나 실행 중인 앱에 손대기 더 쉽게 만듭니다.',
|
|
25
32
|
baseline:
|
|
26
|
-
'
|
|
33
|
+
'이 도구가 찾는 누수는 나오지 않았습니다. 그래도 코드의 구조, 그 안의 글과 값, 서버 주소는 읽힙니다. 어떤 앱이든 지는 위험입니다.',
|
|
34
|
+
unrated: '출시물의 본체를 읽지 못해 판정하지 않았습니다. 새는 곳이 없다는 뜻이 아닙니다.',
|
|
27
35
|
},
|
|
28
36
|
andMore: (count) => `외 ${count}개`,
|
|
29
37
|
originals: (names) => `원본: ${names}`,
|
|
38
|
+
notSigned: '서명 없음',
|
|
39
|
+
namesIn: (architecture, count) => `${architecture}: 이름 ${count}개`,
|
|
40
|
+
sourcesIn: (count, sample) => `원본 경로 ${count}개, 예: ${sample}`,
|
|
30
41
|
// Listed roughly from most to least damaging: a report shows the rules in this order.
|
|
31
42
|
rules: {
|
|
32
43
|
'stray-file': {
|
|
33
44
|
title: '배포물에 있으면 안 되는 파일이 섞여 있습니다',
|
|
34
|
-
body: () => [
|
|
35
|
-
'설정 파일, 저장소 폴더, 열쇠 보관 파일, DB
|
|
36
|
-
'
|
|
45
|
+
body: ({ debugSymbols }) => [
|
|
46
|
+
'설정 파일, 저장소 폴더, 열쇠 보관 파일, DB 파일, 디버그 정보는 그 자체로 원본이나 비밀을 담고 있습니다.',
|
|
47
|
+
'배포물에서 빼고, 어디서 섞여 들어오는지(예: 통째로 복사되는 폴더, 묶을 파일 목록) 확인하세요.',
|
|
48
|
+
...(debugSymbols > 0
|
|
49
|
+
? ['디버그 정보(.dSYM)는 지우지 말고 배포물 밖에 보관하세요. 사용자에게서 온 오류 기록을 풀 때 씁니다.']
|
|
50
|
+
: []),
|
|
37
51
|
],
|
|
38
52
|
},
|
|
39
53
|
secret: {
|
|
@@ -66,10 +80,10 @@ const KO = {
|
|
|
66
80
|
],
|
|
67
81
|
},
|
|
68
82
|
'macos-entitlement': {
|
|
69
|
-
title: '
|
|
83
|
+
title: '실행 중인 앱의 속을 여는 권한이 켜진 채 서명됐습니다',
|
|
70
84
|
body: () => [
|
|
71
|
-
'get-task-allow
|
|
72
|
-
'출시용
|
|
85
|
+
'get-task-allow 는 다른 프로그램이 실행 중인 앱에 붙어 속을 들여다보게 합니다. disable-library-validation 은 누가 서명했든 플러그인과 라이브러리를 앱 안에 싣게 합니다.',
|
|
86
|
+
'꼭 필요한 것이 아니면 출시용 서명의 권한 목록(entitlements)에서 빼세요.',
|
|
73
87
|
],
|
|
74
88
|
},
|
|
75
89
|
'sourcemap-names': {
|
|
@@ -79,11 +93,22 @@ const KO = {
|
|
|
79
93
|
'압축된 코드를 원래 구조로 되짚기가 쉬워집니다.',
|
|
80
94
|
],
|
|
81
95
|
},
|
|
96
|
+
'native-symbols': {
|
|
97
|
+
title: '실행 파일에 개발할 때 쓰는 이름표가 그대로 남아 있습니다',
|
|
98
|
+
body: ({ files, names, sources, builders }) => [
|
|
99
|
+
`실행 파일 ${files}개에 함수와 변수 이름 ${names}개가 남아 있습니다. 코드가 무슨 일을 하는지 이름만으로 읽힙니다.`,
|
|
100
|
+
...(builders > 0 ? ['원본 파일의 경로도 들어 있어, 빌드한 컴퓨터의 계정 이름과 폴더 구조가 그대로 보입니다.'] : []),
|
|
101
|
+
...(sources > 0 && builders === 0 ? ['원본 파일의 경로도 들어 있어, 코드의 폴더 구조가 그대로 보입니다.'] : []),
|
|
102
|
+
'서명하기 전에 strip 으로 떼세요. 실행 파일은 옵션 없는 strip 이 이름을 모두 떼고, 다른 파일이 불러 쓰는 라이브러리는 strip -x 가 밖에서 쓰는 이름만 남깁니다. strip -S 는 경로와 디버그 표식만 뗍니다.',
|
|
103
|
+
'떼기 전에 디버그 정보(.dSYM)를 배포물 밖에 보관해 두면 오류 기록은 계속 풀 수 있습니다.',
|
|
104
|
+
'뗀 뒤 reaproof 를 다시 돌려 이 항목이 사라지면 된 것입니다. 직접 세어 보려면 nm --no-dyldinfo -a <파일> 을 쓰세요. 옵션 없는 nm 은 이름을 뗀 프로그램에 <redacted function N> 을 지어 붙입니다.',
|
|
105
|
+
],
|
|
106
|
+
},
|
|
82
107
|
'macos-runtime': {
|
|
83
108
|
title: '강화 실행 환경(Hardened Runtime)이 꺼져 있습니다',
|
|
84
109
|
body: () => [
|
|
85
|
-
'꺼져
|
|
86
|
-
'서명할 때
|
|
110
|
+
'꺼져 있거나 서명이 없으면 실행 중인 앱에 다른 코드를 끼워 넣기 쉽습니다.',
|
|
111
|
+
'서명할 때 켜세요(codesign 은 --options runtime). 실행 파일마다 따로 서명되므로 앱 안의 도우미 프로그램도 확인하세요.',
|
|
87
112
|
],
|
|
88
113
|
},
|
|
89
114
|
'electron-fuse': {
|
|
@@ -114,6 +139,13 @@ const KO = {
|
|
|
114
139
|
'빌드 도구의 압축(minify) 옵션을 켜세요. 읽는 수고를 늘릴 뿐 숨겨 주지는 않습니다.',
|
|
115
140
|
],
|
|
116
141
|
},
|
|
142
|
+
'developer-file': {
|
|
143
|
+
title: '개발하는 사람이 보려고 쓴 문서와 스크립트가 함께 들어 있습니다',
|
|
144
|
+
body: ({ files }) => [
|
|
145
|
+
`문서와 스크립트 파일 ${files}개가 들어 있습니다. 웹사이트라면 올라가는 순간 누구나 주소로 열어 볼 수 있습니다.`,
|
|
146
|
+
'올릴 필요가 없으면 배포에서 빼세요(예: 호스팅의 제외 목록, 묶을 파일 목록). 일부러 넣은 것이면 그대로 두면 됩니다.',
|
|
147
|
+
],
|
|
148
|
+
},
|
|
117
149
|
endpoint: {
|
|
118
150
|
title: '코드에서 그대로 읽히는 서버 주소',
|
|
119
151
|
body: ({ addresses }) => [
|
|
@@ -162,22 +194,42 @@ const KO = {
|
|
|
162
194
|
limits: [
|
|
163
195
|
'화면 구조와 서버 요청 주소는 사용자 기기로 내려가는 이상 가릴 수 없습니다.',
|
|
164
196
|
'가격 계산, 권한 판단처럼 베껴지면 안 되는 로직은 서버에서 계산하세요.',
|
|
165
|
-
'실제로 무엇이 읽히는지는 reaproof measure 로 볼 수 있습니다.',
|
|
166
197
|
],
|
|
198
|
+
namesThatStay:
|
|
199
|
+
'실행 파일은 이름을 떼어도 실행에 필요한 이름은 남습니다. 불러 쓰는 라이브러리 함수의 이름이 그렇고, Swift 와 Objective-C 에서는 자기 타입, 속성, 클래스, 메서드 이름도 남습니다.',
|
|
200
|
+
measureHint: 'JavaScript 에서 실제로 무엇이 읽히는지는 reaproof measure 로 볼 수 있습니다.',
|
|
167
201
|
deployedScope: '이 점검은 배포된 지도 파일만 봅니다. 열쇠와 섞인 파일은 결과물 폴더에서 reaproof 로 점검하세요.',
|
|
168
202
|
skippedOtherOrigins: (count) => `다른 주소에서 불러오는 파일 ${count}개는 보지 않았습니다.`,
|
|
169
203
|
skippedLarge: (count) => `너무 커서 건너뛴 파일 ${count}개`,
|
|
170
204
|
unreadableArchives: (count) => `읽지 못한 ASAR 꾸러미 ${count}개`,
|
|
205
|
+
unopenedContainers: (count, names) =>
|
|
206
|
+
`열지 못한 꾸러미 ${count}개(${names}): 이 종류는 안을 읽지 못합니다. 안에 든 앱이나 폴더를 꺼내 직접 가리켜 주세요.`,
|
|
207
|
+
diskImageHint: 'DMG 는 hdiutil attach -readonly -nobrowse <파일> 로 연 뒤, /Volumes 아래에 나타난 앱을 가리키면 됩니다.',
|
|
208
|
+
textless: (count) => `그림, 소리, 영상, 글꼴, 압축 파일처럼 읽어도 찾을 것이 없는 ${count}개는 읽지 않았습니다.`,
|
|
171
209
|
errors: {
|
|
172
210
|
'path-not-found': (path) => [`경로를 찾지 못했습니다: ${path}`],
|
|
173
|
-
'no-build-output': (path, buildCommand
|
|
211
|
+
'no-build-output': (path, { buildCommand }) => [
|
|
174
212
|
`빌드 결과물을 찾지 못했습니다: ${path}`,
|
|
175
|
-
`먼저 빌드하세요(${buildCommand}). 결과물 폴더를 직접 알려 줘도 됩니다(예: reaproof dist).`,
|
|
213
|
+
`먼저 빌드하세요(${buildCommand?.join(' ') ?? 'npm run build'}). 결과물 폴더를 직접 알려 줘도 됩니다(예: npx reaproof dist).`,
|
|
214
|
+
],
|
|
215
|
+
'artifact-needed': (path, { projectFile }) => [
|
|
216
|
+
`이 폴더는 소스 프로젝트로 보입니다(${projectFile}): ${path}`,
|
|
217
|
+
'무엇이 출시물인지는 짐작하지 않습니다. 출시물을 직접 가리켜 주세요(예: npx reaproof 경로/MyApp.app).',
|
|
176
218
|
],
|
|
177
219
|
'build-failed': () => ['빌드가 끝까지 되지 않았습니다. 위에 나온 오류를 먼저 해결하세요.'],
|
|
178
220
|
'page-unreachable': (url) => [`주소를 열지 못했습니다: ${url}`],
|
|
179
221
|
'local-only': () => ['이 명령은 주소에는 쓸 수 없습니다. 결과물 폴더에서 실행하세요.'],
|
|
180
222
|
},
|
|
223
|
+
because: (reason) => `이유: ${reason}`,
|
|
224
|
+
unreachableReasons: {
|
|
225
|
+
INVALID_ADDRESS: '주소 모양이 올바르지 않습니다',
|
|
226
|
+
TIMEOUT: '기다려도 답이 없었습니다',
|
|
227
|
+
TOO_LARGE: '첫 화면이 너무 커서 읽지 않았습니다',
|
|
228
|
+
HTTP: '서버가 오류로 답했습니다',
|
|
229
|
+
ENOTFOUND: '그 이름의 사이트를 찾지 못했습니다',
|
|
230
|
+
ECONNREFUSED: '서버가 연결을 받지 않았습니다',
|
|
231
|
+
ECONNRESET: '연결이 도중에 끊겼습니다. 아직 배포 전이거나 보안 인증서가 준비되지 않은 사이트에서 흔합니다',
|
|
232
|
+
},
|
|
181
233
|
fixPlanTitle: (count) => `지도 파일 ${count}개를 배포물에서 뺍니다:`,
|
|
182
234
|
buildQuestion: (command) => `빌드 결과물이 없습니다. 지금 빌드할까요? (${command}) (y/N) `,
|
|
183
235
|
confirmQuestion: '진행할까요? (y/N) ',
|
|
@@ -209,11 +261,12 @@ const KO = {
|
|
|
209
261
|
},
|
|
210
262
|
measureFailed: (reason) => `rea 를 실행하지 못했습니다: ${reason}`,
|
|
211
263
|
usageError: (reason) => `명령을 이해하지 못했습니다: ${reason}`,
|
|
264
|
+
failOnError: (value) => `--fail-on 은 critical, high, medium 가운데 하나여야 합니다 (받은 값: ${value}).`,
|
|
212
265
|
help: [
|
|
213
266
|
'Reaproof: 출시물에서 쉽게 읽히는 것을 찾아 닫습니다.',
|
|
214
267
|
'',
|
|
215
268
|
'사용법:',
|
|
216
|
-
' reaproof [경로]
|
|
269
|
+
' reaproof [경로] 결과물 폴더나 앱을 점검합니다 (경로를 빼면 지금 폴더)',
|
|
217
270
|
' reaproof <https://주소> 배포된 사이트에 지도 파일이 공개돼 있는지 봅니다',
|
|
218
271
|
' reaproof fix [경로] 지도 파일을 배포물에서 뺍니다 (되돌릴 수 있음)',
|
|
219
272
|
' reaproof undo [경로] 마지막 fix 를 되돌립니다',
|
|
@@ -222,10 +275,17 @@ const KO = {
|
|
|
222
275
|
'옵션:',
|
|
223
276
|
' --json 결과를 JSON 으로 출력',
|
|
224
277
|
' -y, --yes fix 와 measure 를 묻지 않고 진행',
|
|
278
|
+
' --fail-on 등급 이 등급부터 종료 코드 1 (critical, high, medium 중 하나. 기본은 high)',
|
|
225
279
|
' --lang ko|en 출력 언어',
|
|
226
280
|
' -h, --help 도움말',
|
|
227
281
|
' -v, --version 버전',
|
|
228
282
|
'',
|
|
283
|
+
'종료 코드:',
|
|
284
|
+
' 0 멈출 누수 없음 (보통 등급은 --fail-on medium 을 줘야 1 이 됩니다)',
|
|
285
|
+
' 1 --fail-on 등급 이상의 누수 있음',
|
|
286
|
+
' 2 쓰는 법이 틀렸거나 대상을 정하지 못함',
|
|
287
|
+
' 3 읽은 것이 없음 (열지 못한 주소, 열지 못하는 종류의 파일)',
|
|
288
|
+
'',
|
|
229
289
|
'AI 에게 맡길 때:',
|
|
230
290
|
' "npx reaproof --json 을 돌리고 결과대로 고쳐 줘" 라고 하면 됩니다.',
|
|
231
291
|
' 결과 안에 위험도와 항목별 조치가 함께 들어 있습니다.',
|
|
@@ -236,28 +296,42 @@ const EN = {
|
|
|
236
296
|
severity: { critical: 'CRITICAL', high: 'HIGH', medium: 'MEDIUM', info: 'NOTE' },
|
|
237
297
|
severityName: { critical: 'critical', high: 'high', medium: 'medium' },
|
|
238
298
|
target: (paths) => `Target: ${paths}`,
|
|
299
|
+
pickedByName: '(output folders picked by name, next to package.json)',
|
|
239
300
|
scanned: (count) => `Files read: ${count}`,
|
|
301
|
+
ofSeen: (count) => `of ${count} met`,
|
|
302
|
+
programsAmong: (count) => `${count} of them programs`,
|
|
240
303
|
leaksLabel: 'Leaks found',
|
|
241
304
|
noLeaks: 'none',
|
|
242
305
|
leakKinds: (count) => `${count} kind${count === 1 ? '' : 's'}`,
|
|
306
|
+
closings: {
|
|
307
|
+
1: (level, name) => `Exit code 1: a leak at ${name} or above was found (--fail-on ${level}).`,
|
|
308
|
+
3: () => 'Exit code 3: ended without a rating.',
|
|
309
|
+
},
|
|
243
310
|
riskLabel: 'rea risk',
|
|
244
|
-
riskLevels: { 'very-high': 'very high', high: 'high', moderate: 'moderate', baseline: 'baseline' },
|
|
311
|
+
riskLevels: { 'very-high': 'very high', high: 'high', moderate: 'moderate', baseline: 'baseline', unrated: 'not rated' },
|
|
245
312
|
riskSummaries: {
|
|
246
313
|
'very-high': 'A tool like rea can take your original source or your secret values as they are.',
|
|
247
|
-
high: 'Near-secret values, data, or the inside of the running
|
|
248
|
-
moderate: '
|
|
314
|
+
high: 'Near-secret values, data, or the inside of the running program are exposed.',
|
|
315
|
+
moderate: 'No secret value was found, but the findings below make the code easier to read or the running app easier to get into.',
|
|
249
316
|
baseline:
|
|
250
|
-
'
|
|
317
|
+
'None of the leaks this tool looks for was found. Code structure, the text and values in it, and server addresses are still readable, as in any shipped app.',
|
|
318
|
+
unrated: 'What ships could not be read, so no rating is given. That does not mean nothing leaks.',
|
|
251
319
|
},
|
|
252
320
|
andMore: (count) => `and ${count} more`,
|
|
253
321
|
originals: (names) => `originals: ${names}`,
|
|
322
|
+
notSigned: 'not signed',
|
|
323
|
+
namesIn: (architecture, count) => `${architecture}: ${count} names`,
|
|
324
|
+
sourcesIn: (count, sample) => `${count} original path(s), for example ${sample}`,
|
|
254
325
|
// In the same order as the Korean table, which sets the order a report shows them in.
|
|
255
326
|
rules: {
|
|
256
327
|
'stray-file': {
|
|
257
328
|
title: 'Files that should never ship are in the output',
|
|
258
|
-
body: () => [
|
|
259
|
-
'Settings files, repository folders, key stores
|
|
260
|
-
'
|
|
329
|
+
body: ({ debugSymbols }) => [
|
|
330
|
+
'Settings files, repository folders, key stores, database files and debug information carry your source or your secrets as they are.',
|
|
331
|
+
'Take them out of what ships and find out how they get in (a folder copied whole, a packaging file list).',
|
|
332
|
+
...(debugSymbols > 0
|
|
333
|
+
? ['Do not delete the debug information (.dSYM): keep it outside what ships. It is what makes crash reports from users readable.']
|
|
334
|
+
: []),
|
|
261
335
|
],
|
|
262
336
|
},
|
|
263
337
|
secret: {
|
|
@@ -290,10 +364,10 @@ const EN = {
|
|
|
290
364
|
],
|
|
291
365
|
},
|
|
292
366
|
'macos-entitlement': {
|
|
293
|
-
title: 'The app is signed with
|
|
367
|
+
title: 'The app is signed with entitlements that open it up while it runs',
|
|
294
368
|
body: () => [
|
|
295
|
-
'
|
|
296
|
-
'Leave
|
|
369
|
+
'get-task-allow lets another program attach to the running app and look inside it. disable-library-validation lets the app load plug-ins and libraries whoever signed them.',
|
|
370
|
+
'Leave out of release signing whichever of them the app does not need.',
|
|
297
371
|
],
|
|
298
372
|
},
|
|
299
373
|
'sourcemap-names': {
|
|
@@ -303,11 +377,26 @@ const EN = {
|
|
|
303
377
|
'That makes it much easier to work back from the minified code to its original structure.',
|
|
304
378
|
],
|
|
305
379
|
},
|
|
380
|
+
'native-symbols': {
|
|
381
|
+
title: 'Names meant for development are still in the program',
|
|
382
|
+
body: ({ files, names, sources, builders }) => [
|
|
383
|
+
`${files} program file(s) keep ${names} names of functions and variables. What the code does reads from the names alone.`,
|
|
384
|
+
...(builders > 0
|
|
385
|
+
? ['They also hold the paths of original files, which show the account name and folder layout of the machine that built them.']
|
|
386
|
+
: []),
|
|
387
|
+
...(sources > 0 && builders === 0
|
|
388
|
+
? ['They also hold the paths of original files, which show the folder layout of the code.']
|
|
389
|
+
: []),
|
|
390
|
+
'Strip them before signing. For a program, strip with no options removes every name; for a library that other files load, strip -x keeps only the names used from outside. strip -S removes just the paths and the debugging entries.',
|
|
391
|
+
'Keep the debug information (.dSYM) outside what ships first, and crash reports can still be read.',
|
|
392
|
+
'Run reaproof again afterwards: when this finding is gone, it worked. To count the names yourself, use nm --no-dyldinfo -a <file>; plain nm adds entries of its own, <redacted function N>, to a stripped program.',
|
|
393
|
+
],
|
|
394
|
+
},
|
|
306
395
|
'macos-runtime': {
|
|
307
396
|
title: 'The Hardened Runtime is off',
|
|
308
397
|
body: () => [
|
|
309
|
-
'
|
|
310
|
-
'Turn on the
|
|
398
|
+
'When it is off, or the program is not signed, other code is easier to load into the running app.',
|
|
399
|
+
'Turn it on when signing (codesign --options runtime). Each program is signed by itself, so check the helper programs inside the app too.',
|
|
311
400
|
],
|
|
312
401
|
},
|
|
313
402
|
'electron-fuse': {
|
|
@@ -338,6 +427,13 @@ const EN = {
|
|
|
338
427
|
'Turn on minification in your build tool. It adds reading effort; it does not hide anything.',
|
|
339
428
|
],
|
|
340
429
|
},
|
|
430
|
+
'developer-file': {
|
|
431
|
+
title: 'Notes and scripts written for developers are included',
|
|
432
|
+
body: ({ files }) => [
|
|
433
|
+
`${files} notes or script file(s) are included. On a website, anyone can open them by address once they are deployed.`,
|
|
434
|
+
'Leave them out of the deploy if they do not need to be there (a hosting ignore list, a packaging file list). If they ship on purpose, move on.',
|
|
435
|
+
],
|
|
436
|
+
},
|
|
341
437
|
endpoint: {
|
|
342
438
|
title: 'Server addresses that read straight from the code',
|
|
343
439
|
body: ({ addresses }) => [
|
|
@@ -386,22 +482,43 @@ const EN = {
|
|
|
386
482
|
limits: [
|
|
387
483
|
'Screen structure and the server addresses you call cannot be hidden once they reach a user device.',
|
|
388
484
|
'Compute anything that must not be copied, such as pricing or permission checks, on a server.',
|
|
389
|
-
'To see what is actually readable, run reaproof measure.',
|
|
390
485
|
],
|
|
486
|
+
namesThatStay:
|
|
487
|
+
'A stripped program still carries the names it needs to run: those of the library functions it calls and, in Swift and Objective-C, of its own types, properties, classes and methods.',
|
|
488
|
+
measureHint: 'To see what is actually readable in the JavaScript, run reaproof measure.',
|
|
391
489
|
deployedScope: 'This check looks at deployed source maps only. Check keys and stray files in the build output with reaproof.',
|
|
392
490
|
skippedOtherOrigins: (count) => `Files loaded from other addresses were not checked: ${count}`,
|
|
393
491
|
skippedLarge: (count) => `Files skipped for size: ${count}`,
|
|
394
492
|
unreadableArchives: (count) => `ASAR archives that could not be read: ${count}`,
|
|
493
|
+
unopenedContainers: (count, names) =>
|
|
494
|
+
`Containers that could not be opened: ${count} (${names}). This scanner does not read inside this kind. Take out the app or folder inside and point at that.`,
|
|
495
|
+
diskImageHint: 'For a DMG, open it with hdiutil attach -readonly -nobrowse <file> and point at the app that appears under /Volumes.',
|
|
496
|
+
textless: (count) =>
|
|
497
|
+
`Not read: ${count} file(s) such as pictures, sound, video, fonts and compressed files, in which there is nothing to find.`,
|
|
395
498
|
errors: {
|
|
396
499
|
'path-not-found': (path) => [`Path not found: ${path}`],
|
|
397
|
-
'no-build-output': (path, buildCommand
|
|
500
|
+
'no-build-output': (path, { buildCommand }) => [
|
|
398
501
|
`No build output found in: ${path}`,
|
|
399
|
-
`Build first (${buildCommand}), or point at the output folder (for example: reaproof dist).`,
|
|
502
|
+
`Build first (${buildCommand?.join(' ') ?? 'npm run build'}), or point at the output folder (for example: npx reaproof dist).`,
|
|
503
|
+
],
|
|
504
|
+
'artifact-needed': (path, { projectFile }) => [
|
|
505
|
+
`This folder looks like a source project (${projectFile}): ${path}`,
|
|
506
|
+
'What it ships is not guessed. Point at the thing that ships (for example: npx reaproof path/to/MyApp.app).',
|
|
400
507
|
],
|
|
401
508
|
'build-failed': () => ['The build did not finish. Fix the errors shown above first.'],
|
|
402
509
|
'page-unreachable': (url) => [`Could not open the address: ${url}`],
|
|
403
510
|
'local-only': () => ['This command does not work on an address. Run it on the build output.'],
|
|
404
511
|
},
|
|
512
|
+
because: (reason) => `Reason: ${reason}`,
|
|
513
|
+
unreachableReasons: {
|
|
514
|
+
INVALID_ADDRESS: 'that is not a well-formed address',
|
|
515
|
+
TIMEOUT: 'no answer came in time',
|
|
516
|
+
TOO_LARGE: 'the first page is too large to read',
|
|
517
|
+
HTTP: 'the server answered with an error',
|
|
518
|
+
ENOTFOUND: 'no site with that name was found',
|
|
519
|
+
ECONNREFUSED: 'the server did not accept the connection',
|
|
520
|
+
ECONNRESET: 'the connection was cut off, as often happens with a site that is not deployed yet or whose certificate is not ready',
|
|
521
|
+
},
|
|
405
522
|
fixPlanTitle: (count) => `${count} map file(s) will be taken out of the output:`,
|
|
406
523
|
buildQuestion: (command) => `There is no build output. Build now? (${command}) (y/N) `,
|
|
407
524
|
confirmQuestion: 'Continue? (y/N) ',
|
|
@@ -433,11 +550,12 @@ const EN = {
|
|
|
433
550
|
},
|
|
434
551
|
measureFailed: (reason) => `Could not run rea: ${reason}`,
|
|
435
552
|
usageError: (reason) => `Could not understand the command: ${reason}`,
|
|
553
|
+
failOnError: (value) => `--fail-on takes critical, high or medium (got: ${value}).`,
|
|
436
554
|
help: [
|
|
437
555
|
'Reaproof: finds what is easy to read in what you ship, and closes it.',
|
|
438
556
|
'',
|
|
439
557
|
'Usage:',
|
|
440
|
-
' reaproof [path] check
|
|
558
|
+
' reaproof [path] check a build output or an app (defaults to the current folder)',
|
|
441
559
|
' reaproof <https://site> check whether a deployed site serves its source maps',
|
|
442
560
|
' reaproof fix [path] take map files out of the output (can be undone)',
|
|
443
561
|
' reaproof undo [path] undo the last fix',
|
|
@@ -446,10 +564,17 @@ const EN = {
|
|
|
446
564
|
'Options:',
|
|
447
565
|
' --json print results as JSON',
|
|
448
566
|
' -y, --yes run fix and measure without asking',
|
|
567
|
+
' --fail-on LEVEL exit with 1 from this level up (critical, high or medium; high by default)',
|
|
449
568
|
' --lang ko|en output language',
|
|
450
569
|
' -h, --help help',
|
|
451
570
|
' -v, --version version',
|
|
452
571
|
'',
|
|
572
|
+
'Exit codes:',
|
|
573
|
+
' 0 no leak to stop for (a moderate result is 1 only with --fail-on medium)',
|
|
574
|
+
' 1 a leak at or above the --fail-on level',
|
|
575
|
+
' 2 wrong usage, or no target could be settled on',
|
|
576
|
+
' 3 nothing was read (an address that did not answer, a kind of file that cannot be opened)',
|
|
577
|
+
'',
|
|
453
578
|
'With an AI agent:',
|
|
454
579
|
' Ask it to run "npx reaproof --json" and act on the result.',
|
|
455
580
|
' The result carries the risk level and what to do about each finding.',
|
|
@@ -474,8 +599,9 @@ export function pickLanguage(requested) {
|
|
|
474
599
|
export function createReporter(language) {
|
|
475
600
|
const t = MESSAGES[language];
|
|
476
601
|
const indent = (lines) => lines.map((line) => INDENT + line);
|
|
477
|
-
const
|
|
478
|
-
|
|
602
|
+
const capWith = (items, limit, toLast) =>
|
|
603
|
+
items.length <= limit ? items : [...items.slice(0, limit), toLast(items.length - limit)];
|
|
604
|
+
const capList = (lines, limit) => capWith(lines, limit, (count) => `- ${t.andMore(count)}`);
|
|
479
605
|
|
|
480
606
|
/** "none", or "2 kinds (critical 1, medium 1)": never a score that could read as "safe". */
|
|
481
607
|
function describeLeaks(leaks) {
|
|
@@ -492,9 +618,18 @@ export function createReporter(language) {
|
|
|
492
618
|
if (rule === 'stray-file') return `- ${path} (${t.strayKinds[detail.kind]})`;
|
|
493
619
|
if (rule === 'electron-fuse') return `- ${path}: ${detail.enabled.join(', ')}`;
|
|
494
620
|
if (rule === 'prompt-candidate') return `- ${path}: "${detail.preview}…"`;
|
|
495
|
-
if (
|
|
496
|
-
|
|
621
|
+
if (rule === 'macos-entitlement') return `- ${path}: ${detail.entitlements.join(', ')}`;
|
|
622
|
+
if (rule === 'macos-runtime') {
|
|
623
|
+
// Written the way `codesign -dv` shows it, so the two can be put side by side.
|
|
624
|
+
const slices = detail.slices.map(({ architecture, flags }) => `${architecture}: ${flags ? `flags=${flags}` : t.notSigned}`);
|
|
625
|
+
return `- ${path} (${slices.join('; ')})`;
|
|
626
|
+
}
|
|
627
|
+
if (rule === 'native-symbols') {
|
|
628
|
+
const facts = [t.namesIn(detail.architecture, detail.names)];
|
|
629
|
+
if (detail.sourceCount > 0) facts.push(t.sourcesIn(detail.sourceCount, detail.sampleSources[0]));
|
|
630
|
+
return `- ${path} (${facts.join(', ')})`;
|
|
497
631
|
}
|
|
632
|
+
if (detail.sampleSources?.length > 0) return `- ${path} (${t.originals(detail.sampleSources.join(', '))})`;
|
|
498
633
|
return `- ${path}`;
|
|
499
634
|
}
|
|
500
635
|
|
|
@@ -502,15 +637,55 @@ export function createReporter(language) {
|
|
|
502
637
|
function summarizeRule(findings) {
|
|
503
638
|
// An address list is about the addresses, not about which file each one sits in.
|
|
504
639
|
const addresses = [...new Set(findings.flatMap(({ detail }) => detail.addresses ?? []))];
|
|
640
|
+
const sumOf = (field) => findings.reduce((sum, { detail }) => sum + (detail[field] ?? 0), 0);
|
|
505
641
|
const totals = {
|
|
506
642
|
files: findings.length,
|
|
507
|
-
embedded:
|
|
508
|
-
sources:
|
|
643
|
+
embedded: sumOf('embeddedCount'),
|
|
644
|
+
sources: sumOf('sourceCount'),
|
|
645
|
+
names: sumOf('names'),
|
|
646
|
+
builders: findings.filter(({ detail }) => detail.namesBuilder).length,
|
|
647
|
+
debugSymbols: findings.filter(({ detail }) => detail.kind === 'debug-symbols').length,
|
|
509
648
|
addresses: addresses.length,
|
|
510
649
|
};
|
|
511
650
|
return { addresses, totals };
|
|
512
651
|
}
|
|
513
652
|
|
|
653
|
+
/** What was met and could not be read. It changes how everything after it reads, so it comes first. */
|
|
654
|
+
function listUnread(result) {
|
|
655
|
+
const unread = [];
|
|
656
|
+
if (result.unopenedContainers.length > 0) {
|
|
657
|
+
const names = capWith(result.unopenedContainers, MAX_LISTED_PATHS, t.andMore);
|
|
658
|
+
unread.push(t.unopenedContainers(result.unopenedContainers.length, names.join(', ')));
|
|
659
|
+
if (result.unopenedContainers.some((name) => /\.dmg$/i.test(name))) unread.push(t.diskImageHint);
|
|
660
|
+
}
|
|
661
|
+
if (result.skippedLargeFiles.length > 0) unread.push(t.skippedLarge(result.skippedLargeFiles.length));
|
|
662
|
+
if (result.unreadableArchives.length > 0) unread.push(t.unreadableArchives(result.unreadableArchives.length));
|
|
663
|
+
if (result.skippedOtherOrigins > 0) unread.push(t.skippedOtherOrigins(result.skippedOtherOrigins));
|
|
664
|
+
// On disk, what is left after those was passed over for its kind.
|
|
665
|
+
const namedCount = result.unopenedContainers.length + result.skippedLargeFiles.length + result.unreadableArchives.length;
|
|
666
|
+
const passedOver = result.filesSeen - result.filesScanned - namedCount;
|
|
667
|
+
if (!result.isDeployed && passedOver > 0) unread.push(t.textless(passedOver));
|
|
668
|
+
return unread;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* What cannot be hidden is said about what was read: with nothing read it is beside the
|
|
673
|
+
* point, and what holds for programs is said where a program was read. `measure` runs
|
|
674
|
+
* rea's JavaScript analysis on files someone holds, so it is offered only where that applies.
|
|
675
|
+
*/
|
|
676
|
+
function listLimits(result) {
|
|
677
|
+
if (result.risk === 'unrated') return [];
|
|
678
|
+
const limits = [...t.limits];
|
|
679
|
+
if (result.programsScanned > 0) limits.push(t.namesThatStay);
|
|
680
|
+
if (result.scriptsScanned > 0 && !result.isDeployed) limits.push(t.measureHint);
|
|
681
|
+
return limits;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function describeUnreachable(reason) {
|
|
685
|
+
const kind = reason.startsWith('HTTP ') ? 'HTTP' : reason;
|
|
686
|
+
return Object.hasOwn(t.unreachableReasons, kind) ? `${t.unreachableReasons[kind]} (${reason})` : reason;
|
|
687
|
+
}
|
|
688
|
+
|
|
514
689
|
function renderRule(rule, findings, isDeployed) {
|
|
515
690
|
const { addresses, totals } = summarizeRule(findings);
|
|
516
691
|
const listed =
|
|
@@ -557,15 +732,21 @@ export function createReporter(language) {
|
|
|
557
732
|
const targets = isDeployed
|
|
558
733
|
? result.root
|
|
559
734
|
: result.targets.map((target) => toDisplayPath(result.root, target)).join(', ');
|
|
735
|
+
const asides = [];
|
|
736
|
+
if (result.filesSeen > result.filesScanned) asides.push(t.ofSeen(result.filesSeen));
|
|
737
|
+
if (result.programsScanned > 0) asides.push(t.programsAmong(result.programsScanned));
|
|
738
|
+
const scanned = t.scanned(result.filesScanned);
|
|
560
739
|
const lines = [
|
|
561
|
-
t.target(targets),
|
|
562
|
-
|
|
740
|
+
result.targetsPickedByName ? `${t.target(targets)} ${t.pickedByName}` : t.target(targets),
|
|
741
|
+
asides.length > 0 ? `${scanned} (${asides.join(', ')})` : scanned,
|
|
742
|
+
...indent(listUnread(result)),
|
|
563
743
|
'',
|
|
564
744
|
`${t.riskLabel}: ${t.riskLevels[result.risk]}`,
|
|
565
745
|
INDENT + t.riskSummaries[result.risk],
|
|
566
|
-
`${t.leaksLabel}: ${describeLeaks(result.leaks)}`,
|
|
567
|
-
'',
|
|
568
746
|
];
|
|
747
|
+
// With nothing read, "none found" would read as "clean".
|
|
748
|
+
if (result.risk !== 'unrated') lines.push(`${t.leaksLabel}: ${describeLeaks(result.leaks)}`);
|
|
749
|
+
lines.push('');
|
|
569
750
|
|
|
570
751
|
for (const rule of Object.keys(t.rules)) {
|
|
571
752
|
const findings = findingsByRule.get(rule);
|
|
@@ -574,16 +755,10 @@ export function createReporter(language) {
|
|
|
574
755
|
if (result.findings.some(({ rule }) => SOURCE_MAP_RULES.has(rule))) {
|
|
575
756
|
lines.push(...nextBuildHints(result.frameworks), '');
|
|
576
757
|
}
|
|
577
|
-
|
|
578
|
-
if (
|
|
579
|
-
if (
|
|
580
|
-
|
|
581
|
-
lines.push('', t.skippedLarge(result.skippedLargeFiles.length));
|
|
582
|
-
}
|
|
583
|
-
if (result.unreadableArchives.length > 0) {
|
|
584
|
-
lines.push('', t.unreadableArchives(result.unreadableArchives.length));
|
|
585
|
-
}
|
|
586
|
-
return lines.join('\n');
|
|
758
|
+
const limits = listLimits(result);
|
|
759
|
+
if (limits.length > 0) lines.push(`[${t.severity.info}] ${t.limitsTitle}`, ...indent(limits), '');
|
|
760
|
+
if (isDeployed) lines.push(t.deployedScope);
|
|
761
|
+
return lines.join('\n').trimEnd();
|
|
587
762
|
},
|
|
588
763
|
|
|
589
764
|
fixPlan(root, mapPaths) {
|
|
@@ -596,6 +771,7 @@ export function createReporter(language) {
|
|
|
596
771
|
t.fixDone(applied.movedCount),
|
|
597
772
|
`${t.riskLabel}: ${t.riskLevels[before.risk]} → ${t.riskLevels[after.risk]}`,
|
|
598
773
|
`${t.leaksLabel}: ${describeLeaks(before.leaks)} → ${describeLeaks(after.leaks)}`,
|
|
774
|
+
...listUnread(after),
|
|
599
775
|
t.backupAt(applied.backupDir),
|
|
600
776
|
t.undoHint,
|
|
601
777
|
'',
|
|
@@ -642,13 +818,21 @@ export function createReporter(language) {
|
|
|
642
818
|
fixCommand: findings.some(isFixable) ? FIX_COMMAND : null,
|
|
643
819
|
};
|
|
644
820
|
}
|
|
645
|
-
return { risk: t.riskSummaries[result.risk], rules, limits:
|
|
821
|
+
return { risk: t.riskSummaries[result.risk], unread: listUnread(result), rules, limits: listLimits(result) };
|
|
822
|
+
},
|
|
823
|
+
|
|
824
|
+
/** @returns {string | null} the line a text answer closes with when the command does not end with 0 */
|
|
825
|
+
closing(exitCode, failOn) {
|
|
826
|
+
return t.closings[exitCode]?.(failOn, t.severityName[failOn]) ?? null;
|
|
646
827
|
},
|
|
647
828
|
|
|
648
|
-
scanError(
|
|
649
|
-
|
|
829
|
+
scanError({ code, path, details }) {
|
|
830
|
+
const lines = t.errors[code](path, details);
|
|
831
|
+
if (details.reason) lines.push(t.because(describeUnreachable(details.reason)));
|
|
832
|
+
return lines.join('\n');
|
|
650
833
|
},
|
|
651
834
|
measureFailed: (error) => t.measureFailed(error.message),
|
|
652
|
-
usageError: (error) =>
|
|
835
|
+
usageError: (error) => t.usageError(error.message),
|
|
836
|
+
failOnError: t.failOnError,
|
|
653
837
|
};
|
|
654
838
|
}
|
package/src/rules.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { checkEndpoints, checkPromptCandidates, checkUnminifiedCode } from './checks/content.mjs';
|
|
2
|
-
import { checkStrayFile, checkUnshippedSource } from './checks/paths.mjs';
|
|
2
|
+
import { checkDeveloperFile, checkStrayFile, checkUnshippedSource } from './checks/paths.mjs';
|
|
3
3
|
import { checkSecrets } from './checks/secrets.mjs';
|
|
4
4
|
import { checkInlineMaps, checkMapFile } from './checks/sourcemaps.mjs';
|
|
5
5
|
|
|
@@ -14,10 +14,12 @@ const MAP_EXTENSIONS = new Set(['.map']);
|
|
|
14
14
|
const SCRIPT_EXTENSIONS = new Set(['.js', '.mjs', '.cjs']);
|
|
15
15
|
const CODE_EXTENSIONS = new Set([...SCRIPT_EXTENSIONS, '.css']);
|
|
16
16
|
const SECRET_SCAN_EXTENSIONS = new Set([...SCRIPT_EXTENSIONS, '.html', '.json', '.pem', '.key']);
|
|
17
|
-
|
|
17
|
+
// Leaks from the worst down. A note (`info`) is not one.
|
|
18
|
+
export const LEAK_SEVERITIES = ['critical', 'high', 'medium'];
|
|
19
|
+
const SEVERITIES_ASCENDING = ['info', ...[...LEAK_SEVERITIES].reverse()];
|
|
18
20
|
const RISK_BY_WORST_SEVERITY = { critical: 'very-high', high: 'high', medium: 'moderate', info: 'baseline' };
|
|
19
21
|
|
|
20
|
-
/** Tells the scanner which files are
|
|
22
|
+
/** Tells the scanner which files are read whole, for every check; the rest are searched for keys only. */
|
|
21
23
|
export function hasContentChecks(extension) {
|
|
22
24
|
return (
|
|
23
25
|
MAP_EXTENSIONS.has(extension) ||
|
|
@@ -26,6 +28,11 @@ export function hasContentChecks(extension) {
|
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
|
|
31
|
+
/** JavaScript is what `measure` can show more of, so the scanner keeps count of it. */
|
|
32
|
+
export function isScript(extension) {
|
|
33
|
+
return SCRIPT_EXTENSIONS.has(extension);
|
|
34
|
+
}
|
|
35
|
+
|
|
29
36
|
/** @param {{ severity: Severity }[]} findings */
|
|
30
37
|
export function worstSeverity(findings) {
|
|
31
38
|
const rank = (severity) => SEVERITIES_ASCENDING.indexOf(severity);
|
|
@@ -47,19 +54,28 @@ export function groupByRule(findings) {
|
|
|
47
54
|
* The two things a result says at a glance.
|
|
48
55
|
*
|
|
49
56
|
* `risk` is how exposed the build is to an agent with a tool like rea. Its lowest level is
|
|
50
|
-
* "baseline", never "safe":
|
|
51
|
-
*
|
|
57
|
+
* "baseline", never "safe": code structure, text and server addresses are readable in any
|
|
58
|
+
* shipped app. When what ships could not be read and no leak showed anyway, there is no
|
|
59
|
+
* level at all: "no leak found" would be a statement about nothing.
|
|
52
60
|
*
|
|
53
61
|
* `leaks` counts kinds of leak, each at its worst severity: three hundred exposed maps are
|
|
54
62
|
* the same mistake as one, not three hundred times as bad. Notes are not leaks.
|
|
63
|
+
*
|
|
64
|
+
* @param {boolean} wasRead whether the scan got to read what ships
|
|
55
65
|
*/
|
|
56
|
-
export function summarizeFindings(findings) {
|
|
57
|
-
const leaks =
|
|
66
|
+
export function summarizeFindings(findings, wasRead) {
|
|
67
|
+
const leaks = Object.fromEntries(LEAK_SEVERITIES.map((severity) => [severity, 0]));
|
|
58
68
|
for (const sameRule of groupByRule(findings).values()) {
|
|
59
69
|
const severity = worstSeverity(sameRule);
|
|
60
70
|
if (severity !== 'info') leaks[severity] += 1;
|
|
61
71
|
}
|
|
62
|
-
|
|
72
|
+
const worst = worstSeverity(findings);
|
|
73
|
+
return { risk: !wasRead && worst === 'info' ? 'unrated' : RISK_BY_WORST_SEVERITY[worst], leaks };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Whether a result's leak counts hold one at this severity or worse. */
|
|
77
|
+
export function hasLeakAtOrAbove(leaks, severity) {
|
|
78
|
+
return LEAK_SEVERITIES.slice(0, LEAK_SEVERITIES.indexOf(severity) + 1).some((worse) => leaks[worse] > 0);
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
/** An ASAR archive is not something to fix, only a fact its owner should know. */
|
|
@@ -74,7 +90,7 @@ export function archiveFinding(path, fileCount) {
|
|
|
74
90
|
* @returns {Finding[]}
|
|
75
91
|
*/
|
|
76
92
|
export function checkPath(file) {
|
|
77
|
-
return [...checkStrayFile(file), ...checkUnshippedSource(file)];
|
|
93
|
+
return [...checkStrayFile(file), ...checkUnshippedSource(file), ...checkDeveloperFile(file)];
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
/**
|