free-coding-models 0.5.87 → 0.5.88
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 +1 -9
- package/changelog/v0.5.88.md +16 -0
- package/package.json +6 -1
- package/src/core/security.js +170 -24
- package/src/core/utils.js +97 -0
- package/web/dist/assets/{index-BpTpPS0g.js → index-DFg1h0Nd.js} +2 -2
- package/web/dist/index.html +1 -1
package/README.md
CHANGED
|
@@ -27,10 +27,6 @@ free-coding-models
|
|
|
27
27
|
<sub>Then create a free account on one of the <a href="#-providers">24 providers</a> to grab an API key.</sub>
|
|
28
28
|
</p>
|
|
29
29
|
|
|
30
|
-
<p align="center">
|
|
31
|
-
<img src="website/public/demo.gif" alt="free-coding-models demo" width="100%">
|
|
32
|
-
</p>
|
|
33
|
-
|
|
34
30
|
<p align="center">
|
|
35
31
|
<sub>Made with ❤️ and ☕ by <a href="https://vanessadepraute.dev">Vanessa Depraute</a> (aka <a href="https://vavanessa.dev">Vava-Nessa</a>)</sub>
|
|
36
32
|
</p>
|
|
@@ -87,10 +83,6 @@ free-coding-models --fiable # print the single most reliable model
|
|
|
87
83
|
- 🔀 Want one endpoint that never dies? `free-coding-models --daemon-bg` starts the [Smart Model Router](./docs/router.md).
|
|
88
84
|
- 🤖 Live inside an agent? Install the [OpenCode plugin](./packages/fcm-opencode/README.md) or the [Pi extension](./packages/fcm-pi/README.md).
|
|
89
85
|
|
|
90
|
-
<p align="center">
|
|
91
|
-
<img src="demo2.gif" alt="free-coding-models TUI demo" width="100%">
|
|
92
|
-
</p>
|
|
93
|
-
|
|
94
86
|
---
|
|
95
87
|
|
|
96
88
|
## 🟢 Providers
|
|
@@ -179,7 +171,7 @@ Flags combine freely in any order. The most common:
|
|
|
179
171
|
| `--reprobe` / `--probe-ttl <ms>` / `--show-broken` | Probe cache control |
|
|
180
172
|
| `--check-drift` | Diff `sources.js` vs models.dev, exit 1 on mismatch |
|
|
181
173
|
| `--config-dir <dir>` | Custom config location |
|
|
182
|
-
| `--fix-permissions` / `--yes` | Auto-fix config file permissions (chmod 600) |
|
|
174
|
+
| `--fix-permissions` / `--yes` | Auto-fix config file permissions (chmod 600, NTFS ACLs via icacls on Windows) |
|
|
183
175
|
| `--web` | Open the Web Dashboard instead of the TUI |
|
|
184
176
|
| `--daemon` / `--daemon-bg` / `--daemon-status` / `--daemon-stop` | Smart Model Router lifecycle |
|
|
185
177
|
| `--sync-set [name]` | Auto-populate a router set with the currently best models |
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Changelog v0.5.88 - 2026-09-06
|
|
2
|
+
|
|
3
|
+
### Fixed
|
|
4
|
+
- **Windows: the config permissions warning no longer re-fires on every launch** (issue #173 follow-up reported by @rutexd). On Windows, Node can only report file modes 0666 (writable) or 0444 (read-only), so the old "is it 600?" check could never pass: the app warned, you answered yes, it said "fixed!", and the next launch warned again in an endless loop.
|
|
5
|
+
- The Windows security verdict now comes from the real NTFS ACL, read with `icacls <file>` and parsed by a new pure helper (`parseIcaclsOutput`). The file counts as protected when NTFS inheritance is disabled and only your user (plus trusted SYSTEM/Administrators entries, matched by name or well-known SID, locale-proof) has access.
|
|
6
|
+
- Fixing on Windows now runs `icacls "<file>" /inheritance:r /grant:r "<user>:F"` (no shell, injection-safe) instead of the no-op chmod, and the fix is **verified by re-reading the ACL** before the app claims success. No more fake "✅ Permissions fixed!".
|
|
7
|
+
- The warning now explains *what* is wrong on NTFS (inherited access, or which other principals have a grant) instead of showing a meaningless octal mode.
|
|
8
|
+
- **Anti-nag guard (all platforms)**: when the fix genuinely cannot be applied or verified (rare), the app now writes a small ack marker next to the config file and stays quiet about the same warning for 30 days, instead of nagging on every single launch. Passing `--fix-permissions` / `--yes` / `-y` always bypasses the marker and retries. The marker is cleaned up as soon as a fix succeeds.
|
|
9
|
+
- The manual fix hint on Windows now shows the exact `icacls` command with your real username, and the POSIX path (chmod 600) additionally re-checks the mode after chmod so success messages are always truthful.
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
- `--fix-permissions` documentation now mentions it applies NTFS ACLs on Windows (README flags table).
|
|
13
|
+
|
|
14
|
+
### For contributors
|
|
15
|
+
- New pure helpers in `src/core/utils.js`, fully unit-tested: `parseIcaclsOutput` (icacls output parser, handles the echoed path glued to the first ACE, `(I)` inheritance markers, SIDs and localized group names) and `shouldSkipSecurityWarn` (30-day anti-nag gate, ISO string or epoch input, clock-skew safe).
|
|
16
|
+
- `checkConfigSecurity()` gained an integration test covering the full lifecycle: warn once, ack, stay silent on relaunch, auto-fix bypasses the ack, marker cleanup after a successful fix.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "free-coding-models",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.88",
|
|
4
4
|
"description": "Find the fastest coding LLM models in seconds — ping free models from multiple providers, pick the best one for OpenCode, Cursor, or any AI coding assistant.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"nvidia",
|
|
@@ -94,5 +94,10 @@
|
|
|
94
94
|
"react": "^19.2.7",
|
|
95
95
|
"react-dom": "^19.2.7",
|
|
96
96
|
"vite": "^8.0.16"
|
|
97
|
+
},
|
|
98
|
+
"pnpm": {
|
|
99
|
+
"onlyBuiltDependencies": [
|
|
100
|
+
"esbuild"
|
|
101
|
+
]
|
|
97
102
|
}
|
|
98
103
|
}
|
package/src/core/security.js
CHANGED
|
@@ -17,21 +17,34 @@
|
|
|
17
17
|
* Now checkConfigSecurity() is async, awaited by the bin entry BEFORE the TUI
|
|
18
18
|
* starts, and it never prompts on non-TTY stdin or daemon/web/JSON surfaces.
|
|
19
19
|
*
|
|
20
|
-
* 📖
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
20
|
+
* 📖 Issue #173 follow-up (rutexd): on Windows the "fix" never persisted and the
|
|
21
|
+
* warning re-fired on every launch. Root cause: Node maps win32 modes to only
|
|
22
|
+
* 0666 (writable) or 0444 (read-only), so 0600 is unreachable and
|
|
23
|
+
* `(mode & 0o777) === 0o600` was always false; chmod 600 on win32 just clears
|
|
24
|
+
* the read-only bit. The real fix:
|
|
25
|
+
* - the Windows verdict now comes from the NTFS ACL via `icacls <file>`
|
|
26
|
+
* - fixing on Windows runs `icacls /inheritance:r /grant:r <user>:F` and is
|
|
27
|
+
* verified by re-reading the ACL before claiming success
|
|
28
|
+
* - when the fix cannot be applied or verified, an ack marker file
|
|
29
|
+
* (<config>.securityack) keeps the warning quiet for 30 days instead of
|
|
30
|
+
* nagging on every launch (--fix-permissions bypasses the marker)
|
|
31
|
+
* POSIX behaviour (chmod 600) is unchanged on macOS/Linux.
|
|
24
32
|
*
|
|
25
|
-
* 📖
|
|
26
|
-
*
|
|
27
|
-
*
|
|
33
|
+
* 📖 Secure permissions:
|
|
34
|
+
* - POSIX: 0o600 (octal 600) = user:rw, group:---, world:---
|
|
35
|
+
* - Windows: NTFS inheritance disabled, grants only for the current user
|
|
36
|
+
* (SYSTEM/Administrators whitelisted) - see parseIcaclsOutput in utils.js
|
|
28
37
|
*
|
|
29
38
|
* @functions
|
|
30
39
|
* → checkConfigSecurity() - Async main security check; awaited before the TUI starts
|
|
31
40
|
* → resolveSecurityAction() - Pure gate deciding auto-fix / prompt / warn-only (in utils.js)
|
|
41
|
+
* → parseIcaclsOutput() - Pure parser for icacls output (in utils.js)
|
|
42
|
+
* → shouldSkipSecurityWarn() - Pure anti-nag gate for repeat warnings (in utils.js)
|
|
32
43
|
* → getConfigPermissions() - Returns file mode object for config
|
|
33
|
-
* → isConfigSecure() - Boolean check if permissions are correct
|
|
34
|
-
* → fixConfigPermissions() - Applies chmod 600 to config file (
|
|
44
|
+
* → isConfigSecure() - Boolean check if permissions are correct (POSIX modes)
|
|
45
|
+
* → fixConfigPermissions() - Applies chmod 600 to config file (POSIX)
|
|
46
|
+
* → getWindowsAclStatus() - Async icacls read + parse → is the ACL secure?
|
|
47
|
+
* → fixWindowsAcl() - Async icacls fix + ACL re-verify
|
|
35
48
|
* → promptSecurityFix() - Interactive prompt asking user to fix permissions
|
|
36
49
|
*
|
|
37
50
|
* @exports checkConfigSecurity, isConfigSecure, fixConfigPermissions, formatMode, formatModeRwx
|
|
@@ -41,8 +54,12 @@ import fs from 'node:fs'
|
|
|
41
54
|
import path from 'node:path'
|
|
42
55
|
import os from 'node:os'
|
|
43
56
|
import readline from 'node:readline'
|
|
57
|
+
import { execFile } from 'node:child_process'
|
|
58
|
+
import { promisify } from 'node:util'
|
|
44
59
|
import { CONFIG_PATH } from './config.js'
|
|
45
|
-
import { resolveSecurityAction } from './utils.js'
|
|
60
|
+
import { resolveSecurityAction, parseIcaclsOutput, shouldSkipSecurityWarn } from './utils.js'
|
|
61
|
+
|
|
62
|
+
const execFileAsync = promisify(execFile)
|
|
46
63
|
|
|
47
64
|
// 📖 Config file path - matches the path used in config.js (honours the
|
|
48
65
|
// 📖 --config-dir / FCM_CONFIG_DIR override when set).
|
|
@@ -91,7 +108,8 @@ export function isConfigSecure() {
|
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
// 📖 Fix config file permissions to secure mode (chmod 600)
|
|
94
|
-
// 📖
|
|
111
|
+
// 📖 POSIX path; on Windows this is NOT the real fix (see fixWindowsAcl) and is
|
|
112
|
+
// 📖 kept only as a harmless last-ditch fallback. Returns true if successful.
|
|
95
113
|
export function fixConfigPermissions() {
|
|
96
114
|
const configPath = getConfigPath()
|
|
97
115
|
|
|
@@ -107,6 +125,88 @@ export function fixConfigPermissions() {
|
|
|
107
125
|
}
|
|
108
126
|
}
|
|
109
127
|
|
|
128
|
+
// 📖 Current Windows user name, used both for icacls grants and for matching
|
|
129
|
+
// 📖 ACE lines when parsing the ACL (os.userInfo works on win32 too).
|
|
130
|
+
function getWindowsUserName() {
|
|
131
|
+
try {
|
|
132
|
+
return os.userInfo().username
|
|
133
|
+
} catch {
|
|
134
|
+
return process.env.USERNAME || ''
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// 📖 Read the real NTFS ACL via icacls and decide if it is secure:
|
|
139
|
+
// 📖 inheritance disabled + grants only for the current user (and trusted
|
|
140
|
+
// 📖 SYSTEM/Administrators entries). Returns { checked, secure, ...parsed };
|
|
141
|
+
// 📖 checked=false when icacls is missing or failed (caller falls back to the
|
|
142
|
+
// 📖 ack-gated warning path instead of trusting useless mode bits).
|
|
143
|
+
async function getWindowsAclStatus(configPath) {
|
|
144
|
+
try {
|
|
145
|
+
const { stdout } = await execFileAsync('icacls', [configPath], {
|
|
146
|
+
timeout: 5000,
|
|
147
|
+
windowsHide: true,
|
|
148
|
+
})
|
|
149
|
+
const parsed = parseIcaclsOutput({ output: stdout, userName: getWindowsUserName(), filePath: configPath })
|
|
150
|
+
const secure = !parsed.inheritanceEnabled && !parsed.othersHaveAccess && parsed.ownerHasAccess
|
|
151
|
+
return { checked: true, secure, ...parsed }
|
|
152
|
+
} catch (err) {
|
|
153
|
+
return { checked: false, secure: false, grants: [], otherNames: [] }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 📖 Fix the NTFS ACL for real: drop inherited ACEs, grant only the current
|
|
158
|
+
// 📖 user full control, then VERIFY by re-reading the ACL (never claim success
|
|
159
|
+
// 📖 without proof - that lie was the core of the issue #173 follow-up).
|
|
160
|
+
// 📖 execFile (no shell) keeps the path injection-safe even with spaces.
|
|
161
|
+
async function fixWindowsAcl(configPath) {
|
|
162
|
+
const userName = getWindowsUserName()
|
|
163
|
+
if (!userName) return false
|
|
164
|
+
|
|
165
|
+
try {
|
|
166
|
+
await execFileAsync(
|
|
167
|
+
'icacls',
|
|
168
|
+
[configPath, '/inheritance:r', '/grant:r', `${userName}:F`],
|
|
169
|
+
{ timeout: 5000, windowsHide: true }
|
|
170
|
+
)
|
|
171
|
+
const status = await getWindowsAclStatus(configPath)
|
|
172
|
+
return status.checked && status.secure
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return false
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// 📖 Anti-nag marker: written when we warned but could not fix/verify, so the
|
|
179
|
+
// 📖 same warning does not re-fire on every launch (see shouldSkipSecurityWarn).
|
|
180
|
+
// 📖 Lives next to the config so --config-dir / FCM_CONFIG_DIR stays coherent.
|
|
181
|
+
function getAckPath() {
|
|
182
|
+
return `${getConfigPath()}.securityack`
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function readSecurityAck() {
|
|
186
|
+
try {
|
|
187
|
+
return fs.readFileSync(getAckPath(), 'utf8').trim()
|
|
188
|
+
} catch {
|
|
189
|
+
return null
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function writeSecurityAck() {
|
|
194
|
+
try {
|
|
195
|
+
fs.writeFileSync(getAckPath(), new Date().toISOString(), { mode: 0o600 })
|
|
196
|
+
return true
|
|
197
|
+
} catch {
|
|
198
|
+
return false
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function clearSecurityAck() {
|
|
203
|
+
try {
|
|
204
|
+
fs.unlinkSync(getAckPath())
|
|
205
|
+
} catch {
|
|
206
|
+
// 📖 Nothing to clean up is fine.
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
110
210
|
// 📖 Format permission mode in octal (e.g., 0o644 → "644")
|
|
111
211
|
// 📖 Exported for unit tests
|
|
112
212
|
export function formatMode(mode) {
|
|
@@ -133,7 +233,9 @@ export function formatModeRwx(mode) {
|
|
|
133
233
|
}
|
|
134
234
|
|
|
135
235
|
// 📖 Print the insecure-permissions warning (stderr, so --json stdout stays clean)
|
|
136
|
-
|
|
236
|
+
// 📖 On Windows, mode bits are meaningless (always 0666/0444), so when the ACL
|
|
237
|
+
// 📖 was read we show WHAT actually grants access instead of a fake octal story.
|
|
238
|
+
function printSecurityWarning(perms, aclStatus = { checked: false }) {
|
|
137
239
|
const currentMode = formatMode(perms.mode)
|
|
138
240
|
const currentRwx = formatModeRwx(perms.mode)
|
|
139
241
|
|
|
@@ -145,16 +247,29 @@ function printSecurityWarning(perms) {
|
|
|
145
247
|
console.error('')
|
|
146
248
|
console.error('This means other users on this system may be able to read your API keys.')
|
|
147
249
|
console.error('')
|
|
148
|
-
|
|
250
|
+
|
|
251
|
+
if (aclStatus.checked) {
|
|
252
|
+
if (aclStatus.inheritanceEnabled) {
|
|
253
|
+
console.error('NTFS: this file inherits access permissions from your user folder.')
|
|
254
|
+
}
|
|
255
|
+
if (aclStatus.othersHaveAccess) {
|
|
256
|
+
const names = (aclStatus.otherNames || []).slice(0, 3).join(', ')
|
|
257
|
+
console.error(`NTFS: access is also granted to: ${names}`)
|
|
258
|
+
}
|
|
259
|
+
console.error('')
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
console.error('Recommended: restrict access to your user account only')
|
|
149
263
|
}
|
|
150
264
|
|
|
151
|
-
// 📖 Print the manual fix hint.
|
|
152
|
-
// 📖
|
|
265
|
+
// 📖 Print the manual fix hint. Platform-matched command: icacls on Windows
|
|
266
|
+
// 📖 (NTFS ACLs), chmod elsewhere.
|
|
153
267
|
function printManualFixHint() {
|
|
154
268
|
console.error('')
|
|
155
269
|
if (IS_WINDOWS) {
|
|
270
|
+
const user = getWindowsUserName() || '$env:USERNAME'
|
|
156
271
|
console.error('To fix manually (PowerShell), run:')
|
|
157
|
-
console.error(` icacls "${getConfigPath()}" /inheritance:r /grant:r "$
|
|
272
|
+
console.error(` icacls "${getConfigPath()}" /inheritance:r /grant:r "${user}:F"`)
|
|
158
273
|
} else {
|
|
159
274
|
console.error('To fix manually, run:')
|
|
160
275
|
console.error(` chmod 600 ${getConfigPath()}`)
|
|
@@ -164,24 +279,37 @@ function printManualFixHint() {
|
|
|
164
279
|
|
|
165
280
|
// 📖 Apply the fix and report the outcome. Shared by the prompt path (user said
|
|
166
281
|
// 📖 yes) and the auto-fix path (--fix-permissions / --yes / -y).
|
|
167
|
-
|
|
168
|
-
|
|
282
|
+
// 📖 Windows: icacls ACL fix, verified by re-reading the ACL.
|
|
283
|
+
// 📖 POSIX: chmod 600, verified by re-statting the file.
|
|
284
|
+
// 📖 If the fix cannot be verified, write the anti-nag ack so the warning does
|
|
285
|
+
// 📖 not re-fire on every launch, and point at the manual command.
|
|
286
|
+
async function applyFixAndReport() {
|
|
287
|
+
const configPath = getConfigPath()
|
|
288
|
+
let success = false
|
|
289
|
+
|
|
290
|
+
if (IS_WINDOWS) {
|
|
291
|
+
success = await fixWindowsAcl(configPath)
|
|
292
|
+
} else {
|
|
293
|
+
success = fixConfigPermissions() && getConfigPermissions()?.isSecure === true
|
|
294
|
+
}
|
|
169
295
|
|
|
170
296
|
if (success) {
|
|
297
|
+
clearSecurityAck()
|
|
171
298
|
console.error('')
|
|
172
299
|
console.error('✅ Permissions fixed! Your API keys are now secure.')
|
|
173
300
|
console.error('')
|
|
174
301
|
if (IS_WINDOWS) {
|
|
175
|
-
console.error(
|
|
302
|
+
console.error(`NTFS access is now restricted to "${getWindowsUserName()}".`)
|
|
176
303
|
console.error('')
|
|
177
304
|
}
|
|
178
305
|
return { wasSecure: false, wasFixed: true }
|
|
179
306
|
}
|
|
180
307
|
|
|
308
|
+
writeSecurityAck()
|
|
181
309
|
console.error('')
|
|
182
310
|
console.error('❌ Failed to fix permissions automatically.')
|
|
183
311
|
printManualFixHint()
|
|
184
|
-
return { wasSecure: false, wasFixed: false, error: '
|
|
312
|
+
return { wasSecure: false, wasFixed: false, error: 'fix_failed' }
|
|
185
313
|
}
|
|
186
314
|
|
|
187
315
|
// 📖 Check security and handle the fix flow if needed
|
|
@@ -189,9 +317,14 @@ function applyFixAndReport() {
|
|
|
189
317
|
// 📖 the confirmation prompt are visible and fully resolved before raw mode /
|
|
190
318
|
// 📖 the alternate screen take over.
|
|
191
319
|
//
|
|
320
|
+
// 📖 Windows verdict (issue #173 follow-up): mode bits on win32 are only ever
|
|
321
|
+
// 📖 0666 or 0444, so the POSIX 0600 check would nag forever. The real verdict
|
|
322
|
+
// 📖 comes from the NTFS ACL (icacls). If icacls is unavailable, fall through to
|
|
323
|
+
// 📖 the warning path but let the anti-nag ack keep it to once per 30 days.
|
|
324
|
+
//
|
|
192
325
|
// 📖 Options:
|
|
193
326
|
// autoFix - true when --fix-permissions / --yes / -y was passed: apply the
|
|
194
|
-
// fix without asking
|
|
327
|
+
// fix without asking (also bypasses the anti-nag ack)
|
|
195
328
|
// promptAllowed - false on daemon/web/JSON surfaces: never prompt there, at most
|
|
196
329
|
// warn on stderr
|
|
197
330
|
// stdinIsTTY - override the stdin TTY detection (tests); defaults to real detection
|
|
@@ -205,11 +338,23 @@ export async function checkConfigSecurity(options = {}) {
|
|
|
205
338
|
return { wasSecure: true, wasFixed: false }
|
|
206
339
|
}
|
|
207
340
|
|
|
208
|
-
// 📖
|
|
209
|
-
|
|
341
|
+
// 📖 Security verdict, platform-aware.
|
|
342
|
+
let aclStatus = { checked: false, secure: false, grants: [], otherNames: [] }
|
|
343
|
+
if (IS_WINDOWS) {
|
|
344
|
+
aclStatus = await getWindowsAclStatus(perms.path)
|
|
345
|
+
if (aclStatus.checked && aclStatus.secure) {
|
|
346
|
+
return { wasSecure: true, wasFixed: false }
|
|
347
|
+
}
|
|
348
|
+
} else if (perms.isSecure) {
|
|
210
349
|
return { wasSecure: true, wasFixed: false }
|
|
211
350
|
}
|
|
212
351
|
|
|
352
|
+
// 📖 Anti-nag gate: if we already warned recently and the fix did not stick,
|
|
353
|
+
// 📖 stay quiet instead of nagging on every launch. --fix-permissions bypasses.
|
|
354
|
+
if (options.autoFix !== true && shouldSkipSecurityWarn({ ackedAt: readSecurityAck() })) {
|
|
355
|
+
return { wasSecure: false, wasFixed: false, error: 'warned_recently' }
|
|
356
|
+
}
|
|
357
|
+
|
|
213
358
|
// 📖 Pure gate (see utils.js): decides auto-fix vs prompt vs warn-only.
|
|
214
359
|
const action = resolveSecurityAction({
|
|
215
360
|
configExists: true,
|
|
@@ -225,7 +370,7 @@ export async function checkConfigSecurity(options = {}) {
|
|
|
225
370
|
|
|
226
371
|
// 📖 Security issue detected! Print the warning first so it is on screen
|
|
227
372
|
// 📖 no matter which path follows.
|
|
228
|
-
printSecurityWarning(perms)
|
|
373
|
+
printSecurityWarning(perms, aclStatus)
|
|
229
374
|
|
|
230
375
|
if (action === 'auto-fix') {
|
|
231
376
|
return applyFixAndReport()
|
|
@@ -234,6 +379,7 @@ export async function checkConfigSecurity(options = {}) {
|
|
|
234
379
|
if (action === 'warn-only') {
|
|
235
380
|
console.error('Running non-interactively (piped stdin or daemon/web mode), so skipping the prompt.')
|
|
236
381
|
printManualFixHint()
|
|
382
|
+
writeSecurityAck()
|
|
237
383
|
return { wasSecure: false, wasFixed: false, error: 'non_interactive' }
|
|
238
384
|
}
|
|
239
385
|
|
package/src/core/utils.js
CHANGED
|
@@ -750,6 +750,103 @@ export function resolveSecurityAction({ configExists, isSecure, autoFixRequested
|
|
|
750
750
|
return 'prompt'
|
|
751
751
|
}
|
|
752
752
|
|
|
753
|
+
// 📖 parseIcaclsOutput: PURE parser for Windows `icacls <file>` output, used by
|
|
754
|
+
// 📖 the config security check to get a REAL answer about NTFS access.
|
|
755
|
+
// 📖 WHY: on win32 Node reports POSIX-style modes as 0666 (writable) or 0444
|
|
756
|
+
// 📖 (read-only) only - 0600 is unreachable - so the old `(mode & 0o777) === 0o600`
|
|
757
|
+
// 📖 test was always false and the warning re-fired on every launch (issue #173
|
|
758
|
+
// 📖 follow-up from rutexd). Only the ACL tells the truth on NTFS.
|
|
759
|
+
// 📖
|
|
760
|
+
// 📖 Sample output we must handle (ACEs, one per line, path on the first line):
|
|
761
|
+
// C:\Users\rutex\.free-coding-models.json NT AUTHORITY\SYSTEM:(I)(F)
|
|
762
|
+
// BUILTIN\Administrators:(I)(F)
|
|
763
|
+
// DESKTOP-XYZ\rutex:(F)
|
|
764
|
+
// Successfully processed 1 files; Failed processing 0 files
|
|
765
|
+
// 📖 `(I)` marks an inherited ACE, so "inheritance disabled" = no `(I)` anywhere.
|
|
766
|
+
// 📖 Locale-proof whitelists: names are machine-localized, so we whitelist by
|
|
767
|
+
// 📖 substring (SYSTEM, Administra* covers Administrators/Administrateurs) and
|
|
768
|
+
// 📖 by well-known SID (S-1-5-18 = SYSTEM, S-1-5-32-544 = Administrators), plus
|
|
769
|
+
// 📖 the current user (case-insensitive, domain-qualified or bare).
|
|
770
|
+
// 📖
|
|
771
|
+
// 📖 Params:
|
|
772
|
+
// output - raw stdout of `icacls <path>` (string)
|
|
773
|
+
// userName - current user name (string), e.g. "rutex"
|
|
774
|
+
// filePath - the exact path passed to icacls (optional but STRONGLY
|
|
775
|
+
// recommended: icacls echoes it on the first line, glued to the
|
|
776
|
+
// first ACE, and paths/account names can both contain spaces)
|
|
777
|
+
// 📖 Returns { inheritanceEnabled, othersHaveAccess, ownerHasAccess, grants }
|
|
778
|
+
// grants = [{ name, flags }] for every ACE line; empty when output unparseable.
|
|
779
|
+
export function parseIcaclsOutput({ output, userName, filePath = '' }) {
|
|
780
|
+
const text = String(output ?? '')
|
|
781
|
+
const user = String(userName ?? '').trim().toLowerCase()
|
|
782
|
+
const knownPath = String(filePath ?? '')
|
|
783
|
+
const lines = text.split(/\r?\n/)
|
|
784
|
+
|
|
785
|
+
// 📖 The rights/flags groups always end the line: "(I)(F)", "(F)", "(R,W)"...
|
|
786
|
+
const aceTail = /((?:\([^)]*\)\s*)+)$/
|
|
787
|
+
const grants = []
|
|
788
|
+
for (const rawLine of lines) {
|
|
789
|
+
const tail = aceTail.exec(rawLine)
|
|
790
|
+
if (!tail) continue
|
|
791
|
+
|
|
792
|
+
let before = rawLine.slice(0, tail.index).trim()
|
|
793
|
+
if (before === '') continue
|
|
794
|
+
// 📖 Drop the colon that separates the account name from its flags.
|
|
795
|
+
before = before.replace(/:$/, '').trim()
|
|
796
|
+
// 📖 First ACE shares its line with the echoed path: strip the path we
|
|
797
|
+
// 📖 passed to icacls. Without a known path, keep only the segment after
|
|
798
|
+
// 📖 the last whitespace (exact for simple names; callers should pass
|
|
799
|
+
// 📖 filePath when the path or account name contains spaces).
|
|
800
|
+
if (knownPath && before.startsWith(knownPath)) {
|
|
801
|
+
before = before.slice(knownPath.length).trim()
|
|
802
|
+
} else if (knownPath === '' && /\s/.test(before)) {
|
|
803
|
+
before = before.slice(before.lastIndexOf(' ') + 1).trim()
|
|
804
|
+
}
|
|
805
|
+
if (before === '') continue
|
|
806
|
+
grants.push({ name: before, flags: tail[1].replace(/\s+/g, '') })
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
const isTrusted = (name) => {
|
|
810
|
+
const n = name.toLowerCase()
|
|
811
|
+
if (user !== '' && (n === user || n.endsWith('\\' + user))) return true
|
|
812
|
+
if (n.includes('system') || n.includes('s-1-5-18')) return true
|
|
813
|
+
if (n.includes('administra') || n.includes('s-1-5-32-544')) return true
|
|
814
|
+
return false
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
const inheritanceEnabled = grants.some((g) => g.flags.includes('I'))
|
|
818
|
+
const others = grants.filter((g) => !isTrusted(g.name)).map((g) => g.name)
|
|
819
|
+
const ownerHasAccess = grants.some((g) => {
|
|
820
|
+
const n = g.name.toLowerCase()
|
|
821
|
+
return user !== '' && (n === user || n.endsWith('\\' + user))
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
inheritanceEnabled,
|
|
826
|
+
othersHaveAccess: others.length > 0,
|
|
827
|
+
ownerHasAccess,
|
|
828
|
+
grants,
|
|
829
|
+
otherNames: others,
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
// 📖 shouldSkipSecurityWarn: PURE anti-nag gate for the config security warning.
|
|
834
|
+
// 📖 WHY: when the permission fix cannot be applied or verified (rare: icacls
|
|
835
|
+
// 📖 missing on Windows, chmod failing on POSIX), the insecure state persists and
|
|
836
|
+
// 📖 the warning used to re-fire on every single launch. Once we warned and the
|
|
837
|
+
// 📖 fix did not stick, stay quiet for `intervalDays` (default 30) instead of
|
|
838
|
+
// 📖 nagging daily. `--fix-permissions` bypasses the gate in the caller.
|
|
839
|
+
// 📖 Params: ackedAt - last warning timestamp (ms epoch or ISO string or null)
|
|
840
|
+
// now - current time in ms epoch; intervalDays - quiet window length
|
|
841
|
+
// 📖 Returns true only when ackedAt parses AND is less than intervalDays old.
|
|
842
|
+
export function shouldSkipSecurityWarn({ ackedAt, now = Date.now(), intervalDays = 30 }) {
|
|
843
|
+
if (ackedAt === null || ackedAt === undefined || ackedAt === '') return false
|
|
844
|
+
const then = typeof ackedAt === 'number' ? ackedAt : Date.parse(String(ackedAt))
|
|
845
|
+
if (!Number.isFinite(then)) return false
|
|
846
|
+
const intervalMs = Math.max(0, intervalDays) * 24 * 60 * 60 * 1000
|
|
847
|
+
return now - then < intervalMs && now >= then
|
|
848
|
+
}
|
|
849
|
+
|
|
753
850
|
// 📖 detectTerminalCapabilities: PURE terminal capability probe for TUI overlays.
|
|
754
851
|
// 📖 WHY: basic server consoles (IPMI/KVM viewers, ASPEED framebuffer, serial
|
|
755
852
|
// 📖 terminals) often run 80x24 or smaller with no or limited color support, and
|