@mortz.prk/opencode-auto-mode 1.1.1 → 1.1.2
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.llm.md +9 -3
- package/README.md +7 -1
- package/package.json +1 -1
- package/src/index.ts +1 -1
- package/src/score/files.ts +4 -2
- package/src/score/index.ts +1 -0
- package/src/score/shell-safety.ts +14 -11
- package/src/score/shell.ts +8 -6
- package/src/shell.ts +14 -13
- package/src/tiers.ts +24 -17
package/README.llm.md
CHANGED
|
@@ -178,9 +178,15 @@ are bounded by `reviewTimeoutMs`.
|
|
|
178
178
|
Built-in guards are not a policy language and are not user-extensible. They cover
|
|
179
179
|
protected repository/config paths, credential paths, risky shell structure,
|
|
180
180
|
recursive external deletion, history changes, symlink traversal, project scope,
|
|
181
|
-
and ambiguous syntax.
|
|
182
|
-
`read`/`edit` targets and symlink traversal have a mandatory ask floor. `
|
|
183
|
-
|
|
181
|
+
and ambiguous syntax. Credential/config shell operands are a hard deny; their
|
|
182
|
+
`read`/`edit` targets and symlink traversal have a mandatory ask floor. `.git`
|
|
183
|
+
metadata access, Git global options, and complex/dynamic POSIX shell syntax stay
|
|
184
|
+
HIGH and reach configured decision/LLM stages; static review abstains instead of
|
|
185
|
+
forcing a user prompt. Visible blocked operations still win, and malformed or
|
|
186
|
+
oversized requests remain bounded. Windows shell dialects retain a mandatory
|
|
187
|
+
ask floor. Loading a `skill` is LOW because it only loads instructions; subsequent
|
|
188
|
+
tool calls are checked independently. `glob` and `grep` scope is not verified,
|
|
189
|
+
so static review abstains. `webfetch`, `websearch`,
|
|
184
190
|
`execute`, and unknown tools remain HIGH.
|
|
185
191
|
|
|
186
192
|
Code Mode is bridged in `execute.before` through `ctx.permission.assert`.
|
package/README.md
CHANGED
|
@@ -122,7 +122,13 @@ Stage shapes:
|
|
|
122
122
|
are re-reviewed but never deleted or rewritten.
|
|
123
123
|
3. Built-in guards run first: protected paths and credentials, risky shell
|
|
124
124
|
structure, recursive external deletion, history changes, and ambiguous syntax.
|
|
125
|
-
These guards are not user-extensible.
|
|
125
|
+
These guards are not user-extensible. Git metadata (`.git`), Git global
|
|
126
|
+
options, and complex POSIX shell syntax are left to the configured decision/LLM
|
|
127
|
+
stages, not automatically denied or forced to a user prompt. Credential
|
|
128
|
+
protections and visible blocked operations still take precedence. Unsupported
|
|
129
|
+
Windows shell dialects still require manual approval. Loading a skill is
|
|
130
|
+
low-risk: it loads instructions, not executable code; later tool calls are
|
|
131
|
+
checked separately.
|
|
126
132
|
4. Configured classifiers run in array order. A static stage may resolve low-risk
|
|
127
133
|
work; a decision stage uses OpenCode's native decision API; an LLM stage uses
|
|
128
134
|
its explicit reviewer. The first decisive allow or deny stops the chain.
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,7 +22,7 @@ const INTERRUPTED_REASON = "Auto-mode review was interrupted; please decide your
|
|
|
22
22
|
const PAUSED_REASON = "Auto mode is paused; please decide yourself";
|
|
23
23
|
const INVALID_CONFIG_ACTION = "Auto-mode review is disabled because its configuration is invalid";
|
|
24
24
|
const NATIVE_DENY_REASON = "Native permission deny";
|
|
25
|
-
const REVIEW_ACTIONS = /^(?:read|edit|write|glob|grep|shell|execute|webfetch|websearch|question|todowrite)$/;
|
|
25
|
+
const REVIEW_ACTIONS = /^(?:read|edit|write|glob|grep|shell|execute|webfetch|websearch|question|todowrite|skill)$/;
|
|
26
26
|
const SAFER_ALTERNATIVE = "Continue with a safer alternative.";
|
|
27
27
|
function denialMessage(reason: string): string {
|
|
28
28
|
const trimmed = reason.trim().replace(/[\s.]+$/, "");
|
package/src/score/files.ts
CHANGED
|
@@ -9,12 +9,14 @@ export function isFileAction(action: string): boolean {
|
|
|
9
9
|
return FILE_ACTIONS.has(action);
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/** Credentials/config and symlinks ask; Git metadata goes to model review. */
|
|
13
13
|
function pathApproval(resources: readonly string[], projectDir?: string): RequestScore | undefined {
|
|
14
|
-
if (resources.some((target) => isProtected(target, projectDir)))
|
|
14
|
+
if (resources.some((target) => isProtected(target, projectDir, false)))
|
|
15
15
|
return ask("Protected path access requires human approval");
|
|
16
16
|
if (resources.some((target) => hasSymlinkTraversal(target, projectDir)))
|
|
17
17
|
return ask("Symlink traversal requires human approval");
|
|
18
|
+
if (resources.some((target) => isProtected(target, projectDir)))
|
|
19
|
+
return high("Git metadata access requires further review");
|
|
18
20
|
return undefined;
|
|
19
21
|
}
|
|
20
22
|
|
package/src/score/index.ts
CHANGED
|
@@ -27,6 +27,7 @@ export function scoreRequest(request: PermissionRequest & { worktreeRoot?: strin
|
|
|
27
27
|
// No host allowlist exists, so a URL/query name cannot bound what is fetched.
|
|
28
28
|
if (NETWORK_ACTIONS.has(action)) return high("Network fetch/search has no verified host allowlist");
|
|
29
29
|
if (INTERACTIVE_ACTIONS.has(action)) return low("User interaction without host side effects");
|
|
30
|
+
if (action === "skill") return low("Loading skill instructions without executing them");
|
|
30
31
|
// JavaScript can hide egress behind aliases, templates and computed URLs.
|
|
31
32
|
// Even literal allowlisted fetches do not prove the rest of the program safe.
|
|
32
33
|
if (action === "execute")
|
|
@@ -5,25 +5,25 @@ import { historyChange, historyScore, isGitGlobalOption } from "./history.js";
|
|
|
5
5
|
import { executableShapeScore, pathTargets, type ShellScan } from "./shell-argv.js";
|
|
6
6
|
import { ask, deny, high, type RequestScore } from "./types.js";
|
|
7
7
|
|
|
8
|
-
function commandTouchesProtected(command: ParsedShellCommands | undefined, projectDir?: string): boolean {
|
|
8
|
+
function commandTouchesProtected(command: ParsedShellCommands | undefined, projectDir?: string, includeGit = true): boolean {
|
|
9
9
|
if (!command) return false;
|
|
10
|
-
return command.segments.some((segment) => protectedSegment(segment, projectDir)) ||
|
|
11
|
-
command.leftovers.some((word) => isProtectedShellWord(word, projectDir));
|
|
10
|
+
return command.segments.some((segment) => protectedSegment(segment, projectDir, includeGit)) ||
|
|
11
|
+
command.leftovers.some((word) => isProtectedShellWord(word, projectDir, includeGit));
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
/** A redirection always names a file, so its target takes the full path-aware check. */
|
|
15
|
-
function redirectsProtected(commands: readonly (ParsedShellCommands | undefined)[], projectDir?: string): boolean {
|
|
16
|
-
return commands.some((command) => command?.redirects.some((target) => isProtected(target, projectDir)));
|
|
15
|
+
function redirectsProtected(commands: readonly (ParsedShellCommands | undefined)[], projectDir?: string, includeGit = true): boolean {
|
|
16
|
+
return commands.some((command) => command?.redirects.some((target) => isProtected(target, projectDir, includeGit)));
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** Denies and asks that depend only on the literal parsed command text. */
|
|
20
20
|
function literalShellScore(scan: ShellScan, request: PermissionRequest, worktreeRoot?: string): RequestScore | undefined {
|
|
21
21
|
if (scan.scanned.some((argv) => recursiveExternalDelete(argv, request.projectDir, worktreeRoot)))
|
|
22
22
|
return deny("Recursive/force deletion outside the project");
|
|
23
|
-
//
|
|
24
|
-
//
|
|
23
|
+
// Global options can change cwd/config/helpers: not static clearance, but
|
|
24
|
+
// reviewers can evaluate the full invocation and its effective scope.
|
|
25
25
|
if (scan.scanned.some(isGitGlobalOption))
|
|
26
|
-
return
|
|
26
|
+
return high("Git global options require further review");
|
|
27
27
|
// A history-rewriting invocation is gated once, against the request's VCS facts.
|
|
28
28
|
if (scan.scanned.some(historyChange)) return historyScore(scan.scanned, request.gitState);
|
|
29
29
|
// No regex fallback. Only the guard's literal, fully parsed representation
|
|
@@ -32,17 +32,20 @@ function literalShellScore(scan: ShellScan, request: PermissionRequest, worktree
|
|
|
32
32
|
return undefined;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/**
|
|
35
|
+
/** Credential/config paths deny; Git metadata is left to configured reviewers. */
|
|
36
36
|
function protectedPathScore(scan: ShellScan, projectDir?: string): RequestScore | undefined {
|
|
37
37
|
// Wrappers do not erase destructive semantics (env rm / command git push).
|
|
38
38
|
// Each simple command is judged by its own grammar, so a pattern/script/filter
|
|
39
39
|
// is data even when spelled like a path, while tokens outside any command keep
|
|
40
40
|
// the generic word check.
|
|
41
|
-
if (redirectsProtected(scan.commands, projectDir) ||
|
|
42
|
-
scan.commands.some((command) => commandTouchesProtected(command, projectDir)))
|
|
41
|
+
if (redirectsProtected(scan.commands, projectDir, false) ||
|
|
42
|
+
scan.commands.some((command) => commandTouchesProtected(command, projectDir, false)))
|
|
43
43
|
return deny("Protected path access");
|
|
44
44
|
if (pathTargets(scan).some((arg) => hasSymlinkTraversal(arg, projectDir)))
|
|
45
45
|
return ask("Symlink traversal requires human approval");
|
|
46
|
+
if (redirectsProtected(scan.commands, projectDir) ||
|
|
47
|
+
scan.commands.some((command) => commandTouchesProtected(command, projectDir)))
|
|
48
|
+
return high("Git metadata access requires further review");
|
|
46
49
|
return undefined;
|
|
47
50
|
}
|
|
48
51
|
|
package/src/score/shell.ts
CHANGED
|
@@ -3,21 +3,22 @@ import { guardShell, visibleShellDeny } from "../shell.js";
|
|
|
3
3
|
import { scanShell, type ShellScan } from "./shell-argv.js";
|
|
4
4
|
import { scoreShellBaseline } from "./shell-baseline.js";
|
|
5
5
|
import { shellSafetyScore } from "./shell-safety.js";
|
|
6
|
-
import { ask, deny, type RequestScore } from "./types.js";
|
|
6
|
+
import { ask, deny, high, type RequestScore } from "./types.js";
|
|
7
7
|
|
|
8
|
-
/**
|
|
8
|
+
/** Preserve visible denies; parser uncertainty must reach configured reviewers. */
|
|
9
9
|
function guardScore(guarded: ReviewResult | undefined, resources: readonly string[],
|
|
10
10
|
projectDir?: string, worktreeRoot?: string): RequestScore | undefined {
|
|
11
11
|
if (guarded?.decision === "deny") return deny(guarded.reason);
|
|
12
12
|
if (guarded?.decision !== "ask") return undefined;
|
|
13
|
-
//
|
|
14
|
-
|
|
13
|
+
// The POSIX scanner cannot safely analyze other shell dialects.
|
|
14
|
+
if (process.platform === "win32") return ask(guarded.reason);
|
|
15
|
+
// The parser cannot clear uncertain syntax, but visible dangers still win.
|
|
15
16
|
const blocked = visibleShellDeny(resources, projectDir, worktreeRoot);
|
|
16
17
|
if (blocked !== undefined) return deny(blocked);
|
|
17
|
-
return
|
|
18
|
+
return undefined;
|
|
18
19
|
}
|
|
19
20
|
|
|
20
|
-
/** Scores one shell request:
|
|
21
|
+
/** Scores one shell request: visible denies, safety checks, then residual band. */
|
|
21
22
|
export function scoreShellRequest(request: PermissionRequest, worktreeRoot?: string): RequestScore {
|
|
22
23
|
const { resources, projectDir } = request;
|
|
23
24
|
const guarded = guardShell(resources, projectDir);
|
|
@@ -26,5 +27,6 @@ export function scoreShellRequest(request: PermissionRequest, worktreeRoot?: str
|
|
|
26
27
|
const scan: ShellScan = scanShell(resources);
|
|
27
28
|
const unsafe = shellSafetyScore(scan, request, worktreeRoot);
|
|
28
29
|
if (unsafe) return unsafe;
|
|
30
|
+
if (guarded?.decision === "ask") return high(guarded.reason);
|
|
29
31
|
return scoreShellBaseline(scan, guarded, projectDir, worktreeRoot);
|
|
30
32
|
}
|
package/src/shell.ts
CHANGED
|
@@ -277,17 +277,23 @@ export function parseShellCommands(command: string, limits: { maxChars?: number
|
|
|
277
277
|
* path word is checked with `isProtectedPathOperand` and every other word keeps
|
|
278
278
|
* the generic shell-word check.
|
|
279
279
|
*/
|
|
280
|
-
export function protectedSegment(segment: { words: readonly string[] }, projectDir?: string): boolean {
|
|
280
|
+
export function protectedSegment(segment: { words: readonly string[] }, projectDir?: string, includeGit = true): boolean {
|
|
281
281
|
const roles = operandRoles(segment.words);
|
|
282
282
|
const data = new Set(roles.data);
|
|
283
283
|
const paths = new Set(roles.paths);
|
|
284
|
-
if (roles.pathValues.some((value) => isProtectedPathOperand(value, projectDir))) return true;
|
|
284
|
+
if (roles.pathValues.some((value) => isProtectedPathOperand(value, projectDir, includeGit))) return true;
|
|
285
285
|
return segment.words.some((word, index) => {
|
|
286
286
|
if (data.has(index)) return false;
|
|
287
|
-
return paths.has(index) ? isProtectedPathOperand(word, projectDir) : isProtectedShellWord(word, projectDir);
|
|
287
|
+
return paths.has(index) ? isProtectedPathOperand(word, projectDir, includeGit) : isProtectedShellWord(word, projectDir, includeGit);
|
|
288
288
|
});
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
function protectedCommands(parsed: ParsedLine, projectDir?: string, includeGit = true): boolean {
|
|
292
|
+
return parsed.redirects.some((target) => isProtected(target, projectDir, includeGit)) ||
|
|
293
|
+
parsed.segments.some((segment) => protectedSegment(segment, projectDir, includeGit)) ||
|
|
294
|
+
parsed.leftovers.some((word) => isProtectedShellWord(word, projectDir, includeGit));
|
|
295
|
+
}
|
|
296
|
+
|
|
291
297
|
function analyzeLine(command: string, projectDir?: string): Verdict {
|
|
292
298
|
if (!command.trim()) return { decision: "allow" };
|
|
293
299
|
if (!BALANCED_QUOTES.test(command)) return { decision: "ask" };
|
|
@@ -304,10 +310,8 @@ function analyzeLine(command: string, projectDir?: string): Verdict {
|
|
|
304
310
|
// keeps the protected-path check. Tokens no command consumed (words inside a
|
|
305
311
|
// substitution) keep the generic shell-word check. None of this depends on the
|
|
306
312
|
// line parsing completely: a deny visible in the literal text must win over the
|
|
307
|
-
//
|
|
308
|
-
if (
|
|
309
|
-
segments.some((segment) => protectedSegment(segment, projectDir)) ||
|
|
310
|
-
leftovers.some((word) => isProtectedShellWord(word, projectDir)))
|
|
313
|
+
// parser uncertainty.
|
|
314
|
+
if (protectedCommands({ segments, redirects, leftovers, complete }, projectDir, false))
|
|
311
315
|
return { decision: "deny", reason: "protected path access" };
|
|
312
316
|
// Deny rules that are already visible must win even when later arguments are
|
|
313
317
|
// dynamic (`sudo id $(...)`) and even when the line only parses partially.
|
|
@@ -326,7 +330,7 @@ function analyzeLine(command: string, projectDir?: string): Verdict {
|
|
|
326
330
|
if (!complete || DYNAMIC_SYNTAX.test(command)) return { decision: "ask" };
|
|
327
331
|
|
|
328
332
|
let ask = false;
|
|
329
|
-
let review =
|
|
333
|
+
let review = protectedCommands({ segments, redirects, leftovers, complete }, projectDir);
|
|
330
334
|
for (const segment of segments) {
|
|
331
335
|
const verdict = analyzeSegment(segment);
|
|
332
336
|
if (verdict.decision === "deny") return verdict;
|
|
@@ -518,9 +522,7 @@ export function visibleShellDeny(resources: readonly string[], projectDir?: stri
|
|
|
518
522
|
if (FORK_BOMB.test(visible)) return "dangerous shell operation";
|
|
519
523
|
const parsed = parseShellCommands(visible, { maxChars: MAX_COMMAND_BYTES });
|
|
520
524
|
if (parsed === undefined) continue;
|
|
521
|
-
if (parsed
|
|
522
|
-
parsed.segments.some((segment) => protectedSegment(segment, projectDir)) ||
|
|
523
|
-
parsed.leftovers.some((word) => isProtectedShellWord(word, projectDir)))
|
|
525
|
+
if (protectedCommands(parsed, projectDir, false))
|
|
524
526
|
return "protected path access";
|
|
525
527
|
const commands = parsed.segments.map((segment) => effectiveInvocation(segment.words));
|
|
526
528
|
for (const command of commands) {
|
|
@@ -609,7 +611,6 @@ export function guardShell(resources: readonly string[], projectDir?: string): R
|
|
|
609
611
|
if (verdict.decision === "ask") ask = true;
|
|
610
612
|
else if (verdict.decision === "review") review = true;
|
|
611
613
|
}
|
|
612
|
-
if (ask) return { decision: "ask", reason: "Shell syntax requires
|
|
614
|
+
if (ask) return { decision: "ask", reason: "Shell syntax requires further review" };
|
|
613
615
|
return review ? undefined : { decision: "allow", reason: "read-only command" };
|
|
614
616
|
}
|
|
615
|
-
|
package/src/tiers.ts
CHANGED
|
@@ -83,6 +83,12 @@ function projectRootSpellings(projectDir?: string): string[] {
|
|
|
83
83
|
return roots;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
function protectionSpelling(candidate: string, includeGit?: boolean): string {
|
|
87
|
+
// Metadata needs model review, not a credential deny. Do not normalize here:
|
|
88
|
+
// sensitive descendants and resolved symlink destinations must still count.
|
|
89
|
+
return includeGit === false ? candidate.replace(/(^|[/\\])\.git(?=[/\\]|$)/gi, "$1") : candidate;
|
|
90
|
+
}
|
|
91
|
+
|
|
86
92
|
/**
|
|
87
93
|
* Sensitive spelling of one candidate.
|
|
88
94
|
*
|
|
@@ -92,14 +98,14 @@ function projectRootSpellings(projectDir?: string): string[] {
|
|
|
92
98
|
* still applied to the whole candidate and, in `isProtected`, to every resolved
|
|
93
99
|
* component: a real `.ssh` or `.config` directory stays protected.
|
|
94
100
|
*/
|
|
95
|
-
function sensitiveSpelling(candidate: string, roots: readonly string[]): boolean {
|
|
96
|
-
if (SENSITIVE_PATH.test(candidate)) return true;
|
|
101
|
+
function sensitiveSpelling(candidate: string, roots: readonly string[], includeGit?: boolean): boolean {
|
|
102
|
+
if (SENSITIVE_PATH.test(protectionSpelling(candidate, includeGit))) return true;
|
|
97
103
|
if (path.isAbsolute(candidate)) {
|
|
98
104
|
for (const root of roots) {
|
|
99
|
-
if (isInProject(candidate, root)) return DOT_COMPONENT.test(path.relative(root, path.resolve(candidate)));
|
|
105
|
+
if (isInProject(candidate, root)) return DOT_COMPONENT.test(protectionSpelling(path.relative(root, path.resolve(candidate)), includeGit));
|
|
100
106
|
}
|
|
101
107
|
}
|
|
102
|
-
return DOT_COMPONENT.test(candidate);
|
|
108
|
+
return DOT_COMPONENT.test(protectionSpelling(candidate, includeGit));
|
|
103
109
|
}
|
|
104
110
|
|
|
105
111
|
/** Inspect components before normalization can erase an alias followed by `..`.
|
|
@@ -123,16 +129,17 @@ export function hasSymlinkTraversal(target: string, locationDir = process.cwd())
|
|
|
123
129
|
return false;
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
/** Check lexical
|
|
127
|
-
|
|
132
|
+
/** Check lexical and resolved components. Excluding Git only removes the metadata
|
|
133
|
+
* marker; credentials inside it and symlink/.. traversal remain protected. */
|
|
134
|
+
export function isProtected(target: string, projectDir?: string, includeGit?: boolean): boolean {
|
|
128
135
|
const roots = projectRootSpellings(projectDir);
|
|
129
136
|
const candidates = [target, ...target.split("="), ...target.split(":")];
|
|
130
|
-
if (candidates.some((candidate) => sensitiveSpelling(candidate, roots) ||
|
|
137
|
+
if (candidates.some((candidate) => sensitiveSpelling(candidate, roots, includeGit) ||
|
|
131
138
|
/^\/proc\/(?:self|thread-self|\d+)\/(?:environ|mem)(?:$|\/)/.test(candidate))) return true;
|
|
132
139
|
let current: string;
|
|
133
140
|
try { current = path.isAbsolute(target) ? path.parse(target).root : realpathSync(projectDir ?? process.cwd()); }
|
|
134
141
|
catch { return hasSymlinkTraversal(target, path.resolve(projectDir ?? process.cwd())); }
|
|
135
|
-
const protectedPath = (candidate: string) => SENSITIVE_PATH.test(candidate) ||
|
|
142
|
+
const protectedPath = (candidate: string) => SENSITIVE_PATH.test(protectionSpelling(candidate, includeGit)) ||
|
|
136
143
|
/^\/proc\/(?:self|thread-self|\d+)\/(?:environ|mem)(?:$|\/)/.test(candidate);
|
|
137
144
|
if (protectedPath(current)) return true;
|
|
138
145
|
for (const part of target.split(path.sep).filter(Boolean)) {
|
|
@@ -205,13 +212,13 @@ function operandValue(word: string): string {
|
|
|
205
212
|
* reason to deny `.name`, `.gitignore` or `.cache`. Paths with a separator keep
|
|
206
213
|
* the full check, including resolved components, symlinks and `file:` URLs.
|
|
207
214
|
*/
|
|
208
|
-
export function isProtectedPathOperand(target: string, projectDir?: string): boolean {
|
|
215
|
+
export function isProtectedPathOperand(target: string, projectDir?: string, includeGit?: boolean): boolean {
|
|
209
216
|
if (!target) return false;
|
|
210
217
|
const word = operandValue(target);
|
|
211
218
|
const local = fileUrlPath(word);
|
|
212
|
-
if (local !== undefined) return isProtected(local, projectDir);
|
|
219
|
+
if (local !== undefined) return isProtected(local, projectDir, includeGit);
|
|
213
220
|
if (SCHEME_URL.test(word) || SCP_URL.test(word)) return false;
|
|
214
|
-
return /[/\\]/.test(word) ? isProtected(word, projectDir) : SENSITIVE_PATH.test(word);
|
|
221
|
+
return /[/\\]/.test(word) ? isProtected(word, projectDir, includeGit) : SENSITIVE_PATH.test(protectionSpelling(word, includeGit));
|
|
215
222
|
}
|
|
216
223
|
|
|
217
224
|
/**
|
|
@@ -227,33 +234,33 @@ export function isProtectedPathOperand(target: string, projectDir?: string): boo
|
|
|
227
234
|
* (`signing.pem`) stay protected everywhere, and redirect targets are checked
|
|
228
235
|
* with the full `isProtected` because a redirection always names a file.
|
|
229
236
|
*/
|
|
230
|
-
export function isProtectedShellWord(target: string, projectDir?: string): boolean {
|
|
237
|
+
export function isProtectedShellWord(target: string, projectDir?: string, includeGit?: boolean): boolean {
|
|
231
238
|
if (!target) return false;
|
|
232
239
|
const separator = target.startsWith("-") ? target.indexOf("=") : -1;
|
|
233
240
|
// `--grep=config/auth`, `--format=src/.env` and `--exclude=.env` carry
|
|
234
241
|
// pattern/format data even when the value is spelled like a path.
|
|
235
242
|
if (separator !== -1 && isDataOption(target.slice(0, separator))) return false;
|
|
236
243
|
const local = fileUrlPath(target);
|
|
237
|
-
if (local !== undefined) return isProtected(local, projectDir);
|
|
244
|
+
if (local !== undefined) return isProtected(local, projectDir, includeGit);
|
|
238
245
|
if (SCHEME_URL.test(target) || SCP_URL.test(target)) return false;
|
|
239
246
|
const candidates = [target, ...target.split("="), ...target.split(":")];
|
|
240
247
|
// A `file:` value is a local path even when it arrives through an option.
|
|
241
248
|
const files = candidates.map(fileUrlPath).filter((candidate): candidate is string => candidate !== undefined);
|
|
242
|
-
if (files.length) return files.some((file) => isProtected(file, projectDir));
|
|
249
|
+
if (files.length) return files.some((file) => isProtected(file, projectDir, includeGit));
|
|
243
250
|
// Any other scheme or an SCP-style location is remote, never a local path.
|
|
244
251
|
if (candidates.some((candidate) => SCHEME_URL.test(candidate) || SCP_URL.test(candidate))) return false;
|
|
245
252
|
if (target.startsWith("-")) {
|
|
246
253
|
if (separator === -1) return false;
|
|
247
254
|
// `--config=credentials.json` and `--file=.env` name files.
|
|
248
|
-
return isProtectedPathOperand(target.slice(separator + 1), projectDir);
|
|
255
|
+
return isProtectedPathOperand(target.slice(separator + 1), projectDir, includeGit);
|
|
249
256
|
}
|
|
250
257
|
// `git show HEAD:.env` names a file after the colon; schemes and SCP
|
|
251
258
|
// locations were already excluded above.
|
|
252
|
-
if (!target.includes("=") && target.split(":").slice(1).some((part) => isProtectedPathOperand(part, projectDir)))
|
|
259
|
+
if (!target.includes("=") && target.split(":").slice(1).some((part) => isProtectedPathOperand(part, projectDir, includeGit)))
|
|
253
260
|
return true;
|
|
254
261
|
// A separator-less word is only a file reference when it carries a
|
|
255
262
|
// filename-shaped name; a bare keyword such as `auth` stays data here.
|
|
256
|
-
return /[/\\]/.test(target) ? isProtected(target, projectDir) : SENSITIVE_NAME.test(target);
|
|
263
|
+
return /[/\\]/.test(target) ? isProtected(target, projectDir, includeGit) : SENSITIVE_NAME.test(protectionSpelling(target, includeGit));
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
export function isInProject(target: string, projectDir: string): boolean {
|