opencode-workflow-guard 1.3.2 → 1.3.3
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/package.json +1 -1
- package/src/lib/state.ts +68 -1
- package/src/policies/interpreter.ts +13 -13
- package/src/policies/tamper.ts +1 -1
- package/src/workflow-guard.ts +2 -0
package/package.json
CHANGED
package/src/lib/state.ts
CHANGED
|
@@ -171,6 +171,72 @@ export function resetReviewState(): void {
|
|
|
171
171
|
|
|
172
172
|
export let cachedProjectConfig: ProjectConfig | undefined;
|
|
173
173
|
|
|
174
|
+
/**
|
|
175
|
+
* Strips single-line comments, block comments, and trailing commas from JSONC
|
|
176
|
+
* text without corrupting comment markers inside string literals or URLs.
|
|
177
|
+
*/
|
|
178
|
+
export function stripJsonComments(jsonc: string): string {
|
|
179
|
+
let insideString = false;
|
|
180
|
+
let stringQuote = "";
|
|
181
|
+
let escaped = false;
|
|
182
|
+
let output = "";
|
|
183
|
+
let i = 0;
|
|
184
|
+
|
|
185
|
+
while (i < jsonc.length) {
|
|
186
|
+
const char = jsonc[i]!;
|
|
187
|
+
const next = jsonc[i + 1];
|
|
188
|
+
|
|
189
|
+
if (escaped) {
|
|
190
|
+
output += char;
|
|
191
|
+
escaped = false;
|
|
192
|
+
i++;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (char === "\\") {
|
|
197
|
+
output += char;
|
|
198
|
+
escaped = true;
|
|
199
|
+
i++;
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (char === '"' || char === "'") {
|
|
204
|
+
if (!insideString) {
|
|
205
|
+
insideString = true;
|
|
206
|
+
stringQuote = char;
|
|
207
|
+
} else if (stringQuote === char) {
|
|
208
|
+
insideString = false;
|
|
209
|
+
}
|
|
210
|
+
output += char;
|
|
211
|
+
i++;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (!insideString) {
|
|
216
|
+
if (char === "/" && next === "/") {
|
|
217
|
+
i += 2;
|
|
218
|
+
while (i < jsonc.length && jsonc[i] !== "\n" && jsonc[i] !== "\r") {
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (char === "/" && next === "*") {
|
|
224
|
+
i += 2;
|
|
225
|
+
while (i < jsonc.length && !(jsonc[i] === "*" && jsonc[i + 1] === "/")) {
|
|
226
|
+
i++;
|
|
227
|
+
}
|
|
228
|
+
i += 2;
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
output += char;
|
|
234
|
+
i++;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return output.replace(/,(\s*[}\]])/g, "$1");
|
|
238
|
+
}
|
|
239
|
+
|
|
174
240
|
export function loadProjectConfig(root: string): ProjectConfig {
|
|
175
241
|
const candidates = [
|
|
176
242
|
join(root, ".opencode", "workflow-guard.json"),
|
|
@@ -181,7 +247,8 @@ export function loadProjectConfig(root: string): ProjectConfig {
|
|
|
181
247
|
for (const candidate of candidates) {
|
|
182
248
|
try {
|
|
183
249
|
const raw = readFileSync(candidate, "utf8");
|
|
184
|
-
|
|
250
|
+
const sanitized = stripJsonComments(raw);
|
|
251
|
+
return JSON.parse(sanitized);
|
|
185
252
|
} catch {}
|
|
186
253
|
}
|
|
187
254
|
return {};
|
|
@@ -49,25 +49,25 @@ export function secretPathInPayload(payload: string): string | undefined {
|
|
|
49
49
|
return undefined;
|
|
50
50
|
}
|
|
51
51
|
|
|
52
|
-
const
|
|
53
|
-
/\b(?:
|
|
52
|
+
const FUNCTION_TARGET_PATTERNS: RegExp[] = [
|
|
53
|
+
/\b(?:writeFileSync|writeFile|appendFile|appendFileSync|createWriteStream|unlink|unlinkSync|rmSync|rmdirSync|mkdirSync|renameSync|copyFileSync|cpSync|truncateSync)\s*\(\s*["']([^"']+)["']/g,
|
|
54
|
+
/\b(?:open|os\.remove|os\.unlink|os\.rmdir|os\.mkdir|os\.makedirs|os\.rename|shutil\.rmtree|shutil\.copy|shutil\.copy2|shutil\.move|Path)\s*\(\s*["']([^"']+)["']/g,
|
|
55
|
+
/[>]{1,2}\s*["']?([^\s;&"'|)]+)/g,
|
|
56
|
+
];
|
|
54
57
|
|
|
55
58
|
/**
|
|
56
59
|
* Detects workspace boundary escapes (writes/redirects to paths outside the
|
|
57
|
-
* workspace root) initiated from inline interpreter payloads.
|
|
60
|
+
* workspace root) initiated from inline interpreter payloads. Extracts destination
|
|
61
|
+
* file arguments from file-writing APIs and shell redirects.
|
|
58
62
|
*/
|
|
59
63
|
export function outsideWritePathInPayload(payload: string, root: string): string | undefined {
|
|
60
|
-
const hasWrite = WRITE_VERB_RE.test(payload) || /[>]{1,2}\s*\S/.test(payload);
|
|
61
|
-
if (!hasWrite) return undefined;
|
|
62
|
-
|
|
63
64
|
const candidates = new Set<string>();
|
|
64
|
-
|
|
65
|
-
for (const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
if (m[1]) candidates.add(m[1]);
|
|
65
|
+
|
|
66
|
+
for (const pattern of FUNCTION_TARGET_PATTERNS) {
|
|
67
|
+
const matches = payload.matchAll(pattern);
|
|
68
|
+
for (const m of matches) {
|
|
69
|
+
if (m[1]) candidates.add(m[1]);
|
|
70
|
+
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
for (const candidate of candidates) {
|
package/src/policies/tamper.ts
CHANGED
|
@@ -18,7 +18,7 @@ export const SETTINGS_TAMPER_PATTERNS: RegExp[] = [
|
|
|
18
18
|
new RegExp(`${GT}\\s*["']?[\\w\\/.~-]*(?:${C_LIST})`, "i"),
|
|
19
19
|
new RegExp(`(?:^|\\s)(?:${V_LIST})\\s+[^|;&]*?[\\w\\/.~-]*(?:${U_LIST})`, "i"),
|
|
20
20
|
new RegExp(`${GT}\\s*["']?[\\w\\/.~-]*(?:${U_LIST})`, "i"),
|
|
21
|
-
new RegExp("(?:" + "^|\\s)(?:op" + "encode)\\s+(?:auth|config|permission)\\b", "i"),
|
|
21
|
+
new RegExp("(?:" + "^|\\s)(?:op" + "encode)\\s+(?:-[^|;&]*\\s+)*(?:auth|config|permission)\\b", "i"),
|
|
22
22
|
new RegExp("(?:" + "^|\\s)(?:op" + "encode)\\s+(?:run\\s+)?--auto\\b", "i"),
|
|
23
23
|
];
|
|
24
24
|
|
package/src/workflow-guard.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
sessionVerifyResults,
|
|
45
45
|
loadProjectConfig,
|
|
46
46
|
reloadProjectConfig,
|
|
47
|
+
stripJsonComments,
|
|
47
48
|
isReviewRequired,
|
|
48
49
|
isDocumentationRequired,
|
|
49
50
|
recordReviewResult,
|
|
@@ -64,6 +65,7 @@ export {
|
|
|
64
65
|
resetVerifyState,
|
|
65
66
|
loadProjectConfig,
|
|
66
67
|
reloadProjectConfig,
|
|
68
|
+
stripJsonComments,
|
|
67
69
|
isReviewRequired,
|
|
68
70
|
isDocumentationRequired,
|
|
69
71
|
recordReviewResult,
|