javi-forge 1.34.0 → 1.35.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/assets/claude-hooks/javi-forge-skillguard-pre-tool-use.mjs +25 -2
- package/assets/claude-hooks/manifest.json +1 -1
- package/dist/lib/__fixtures__/fake-secure-fs.d.ts +5 -0
- package/dist/lib/__fixtures__/fake-secure-fs.js +2 -0
- package/dist/lib/claude-hook-manager.js +6 -4
- package/dist/lib/secure-fs-transaction.js +10 -1
- package/package.json +1 -1
|
@@ -87,13 +87,31 @@ function policyPathKeys(input, options = {}) {
|
|
|
87
87
|
return real === lexical ? [lexical] : [lexical, real];
|
|
88
88
|
}
|
|
89
89
|
function isAbsolutePolicyPath(value) { return POSIX_ABSOLUTE.test(value) || WINDOWS_DRIVE.test(value) || WINDOWS_UNC.test(value) || /^\\(?:\\\?|\?\?|\\\.)\\/i.test(value); }
|
|
90
|
+
// Home-relative credential files, matched as a path SUFFIX (never an expanded
|
|
91
|
+
// literal home) so one rule holds for every user, container and HOME. Literals
|
|
92
|
+
// stay lowercase because lexicalNormalize folds keys on darwin/win32.
|
|
93
|
+
const SENSITIVE_PATH_SUFFIXES = Object.freeze(["/.aws/credentials", "/.kube/config", "/.config/gcloud/application_default_credentials.json", "/.docker/config.json", "/.config/gh/hosts.yml"]);
|
|
94
|
+
// Absolute system secrets: the shadow suite holds every local account's password
|
|
95
|
+
// hash, and passwd/vipw leave the "-" backup beside it holding the same secret.
|
|
96
|
+
const SENSITIVE_ABSOLUTE_KEYS = Object.freeze(["/etc/shadow", "/etc/shadow-", "/etc/gshadow", "/etc/gshadow-"]);
|
|
97
|
+
// /proc/<pid>/environ is the FULL environment - every secret - of ANY process the
|
|
98
|
+
// agent can see, so it is a first-class exfiltration sink. The corpus idiom is
|
|
99
|
+
// literal matching; this is the one place a wildcard is unavoidable because the
|
|
100
|
+
// pid component is unbounded. Kept narrow (environ leaves only) and fail-closed:
|
|
101
|
+
// it covers the self/thread-self aliases and the per-thread task/<tid> form.
|
|
102
|
+
const PROC_ENVIRON_KEY = /^\/proc\/(?:\d+|self|thread-self)\/(?:task\/\d+\/)?environ$/;
|
|
103
|
+
// Secret Service / GNOME keyring store: every file under it is credential
|
|
104
|
+
// material, so the directory itself and its whole subtree are sensitive.
|
|
105
|
+
const SENSITIVE_DIRECTORY_SUFFIXES = Object.freeze(["/.local/share/keyrings"]);
|
|
90
106
|
export function isSensitivePolicyKey(key, platform = process.platform) {
|
|
91
107
|
const parts = key.split("/").filter(Boolean);
|
|
92
108
|
const basename = parts.at(-1) ?? "";
|
|
93
109
|
if (/^\.env(?:\..+)?$/i.test(basename) && !/^\.env\.(?:example|sample|template)$/i.test(basename)) return true;
|
|
94
110
|
if ([".npmrc", ".pypirc", ".netrc", ".git-credentials"].includes(basename.toLowerCase())) return true;
|
|
95
111
|
if (parts.some((part) => part === ".ssh" || part === ".gnupg")) return true;
|
|
96
|
-
if (
|
|
112
|
+
if (SENSITIVE_ABSOLUTE_KEYS.includes(key) || PROC_ENVIRON_KEY.test(key)) return true;
|
|
113
|
+
if (SENSITIVE_PATH_SUFFIXES.some((suffix) => key.endsWith(suffix))) return true;
|
|
114
|
+
if (SENSITIVE_DIRECTORY_SUFFIXES.some((directory) => key.endsWith(directory) || key.includes(`${directory}/`))) return true;
|
|
97
115
|
return platform === "win32" || platform === "darwin" ? basename.toLowerCase() === "serviceaccountkey.json" : basename === "serviceAccountKey.json";
|
|
98
116
|
}
|
|
99
117
|
function isManaged(key) {
|
|
@@ -284,7 +302,12 @@ function classifyLongOption(token, longOptions) {
|
|
|
284
302
|
}
|
|
285
303
|
const ASSIGNMENT_TOKEN = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
286
304
|
const SHORT_BUNDLE = /^-[^-]/;
|
|
287
|
-
|
|
305
|
+
// FHS roots whose wholesale destruction bricks the host. CRITICAL_TARGET gates
|
|
306
|
+
// ONLY destructive writes (rm -rf, recursive/777 chmod) and never reads, and
|
|
307
|
+
// membership is exact-token, so `rm -rf /var/tmp/scratch` stays allowed: only the
|
|
308
|
+
// root token itself is protected, in the same bare / trailing / glob forms as "/".
|
|
309
|
+
const CRITICAL_SYSTEM_ROOTS = ["/etc", "/usr", "/var", "/boot"];
|
|
310
|
+
const CRITICAL_TARGET_CORPUS = Object.freeze(["/", "/*", "~", "$HOME", "${HOME}", ".", "..", PROJECT_ROOT, ...CRITICAL_SYSTEM_ROOTS.flatMap((root) => [root, `${root}/`, `${root}/*`])]);
|
|
288
311
|
const isCriticalTarget = (token) => CRITICAL_TARGET_CORPUS.includes(token);
|
|
289
312
|
const OCTAL_MODE_SHAPE = /^0?[0-7]{3,4}$/;
|
|
290
313
|
const SYMBOLIC_MODE_SHAPE = /^[ugoa]*[+=-][rwxXstugo]+(?:,[ugoa]*[+=-][rwxXstugo]+)*$/;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"
|
|
1
|
+
{"schemaVersion":1,"asset":{"name":"javi-forge-skillguard-pre-tool-use.mjs","version":1,"policyVersion":1,"sha256":"5dc2a5c31131f4ac7d8657c78b950de52776aad6eaefe78ea0d764a9963c4425","historical":["78be7e6613c012280b7ad17886462ba166b63ebd031e34565d757b3a0796d7cc"]},"settingsEntries":{"current":{"version":1,"canonicalSha256":"038c59a91bf8967f6908afed74c465f1e7030254e11e4f8738975d6d708424d4"},"historical":[]},"installerHelpers":{"windowsSecureObject":{"name":"javi-forge-windows-secure-object.ps1","sha256":"2289ef6ac6b039ec74dc3ea0894413e243ff9bea963f04008a356b3838f9b8dd"}}}
|
|
@@ -27,6 +27,11 @@ export interface FakeFaults {
|
|
|
27
27
|
writeRefuse?: (name: string, callIndex: number) => boolean;
|
|
28
28
|
/** Refuse renameInDir when the destination base name matches. */
|
|
29
29
|
renameRefuse?: (to: string) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Refuse applyExactMode for a target (chmod fault). Lets a test drive the
|
|
32
|
+
* rollback's prior-mode restore into failure without touching the bytes.
|
|
33
|
+
*/
|
|
34
|
+
applyModeRefuse?: (target: string) => boolean;
|
|
30
35
|
/**
|
|
31
36
|
* Refuse proveManagedContainer for a path on its Nth call (a foreign
|
|
32
37
|
* add/delete-child ACE on a managed container we own — the win32 CREATE_PARENT_DIR
|
|
@@ -152,6 +152,8 @@ export function makeFakeSecureFs() {
|
|
|
152
152
|
return ok();
|
|
153
153
|
},
|
|
154
154
|
async applyExactMode(target, mode) {
|
|
155
|
+
if (fake.faults.applyModeRefuse?.(target))
|
|
156
|
+
return unsafe(`applyMode ${target}`);
|
|
155
157
|
const file = files.get(target);
|
|
156
158
|
if (!file)
|
|
157
159
|
return unsafe(`applyMode enoent ${target}`);
|
|
@@ -191,10 +191,12 @@ function settingsSignals(value, classification, currentAssetSha) {
|
|
|
191
191
|
};
|
|
192
192
|
}
|
|
193
193
|
const REMEDIATION = {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
"
|
|
194
|
+
// User-facing remediation: name the exact command to run, never an internal
|
|
195
|
+
// SDD slice number (which means nothing outside this repo's planning docs).
|
|
196
|
+
absent: "install the managed $ with: javi-forge hooks install claude",
|
|
197
|
+
"released-outdated": "upgrade the managed $ with: javi-forge hooks install claude",
|
|
198
|
+
"exact-legacy": "migrate the legacy $ with: javi-forge hooks install claude",
|
|
199
|
+
"edited-managed": "repair the managed $ with: javi-forge hooks repair claude --force",
|
|
198
200
|
foreign: "manually review the $",
|
|
199
201
|
symlink: "manually review the $",
|
|
200
202
|
"non-regular": "manually review the $",
|
|
@@ -286,7 +286,16 @@ export async function runTransaction(input) {
|
|
|
286
286
|
errors.push(`STOP: cannot stage rollback for ${entry.path}`);
|
|
287
287
|
return;
|
|
288
288
|
}
|
|
289
|
-
|
|
289
|
+
// Prior-mode restore is BEST-EFFORT (JD-B-003): a chmod miss fails
|
|
290
|
+
// toward MORE restrictive perms (the 0600 staging mode), so it is not
|
|
291
|
+
// a security regression and must not halt the rollback — restoring the
|
|
292
|
+
// prior BYTES matters more than the prior mode. Record an
|
|
293
|
+
// INFORMATIONAL note (never `STOP:`, which signals a halted rollback)
|
|
294
|
+
// and continue to the rename.
|
|
295
|
+
const remoded = await secureFs.applyExactMode(path.join(entry.dir.path, rName), entry.prior.mode);
|
|
296
|
+
if (!remoded.ok) {
|
|
297
|
+
errors.push(`note: restored ${entry.path}; prior-mode restore failed, verify permissions (${remoded.detail ?? remoded.refusal ?? "unknown"})`);
|
|
298
|
+
}
|
|
290
299
|
const restored = await secureFs.renameInDir(entry.dir, rName, base);
|
|
291
300
|
if (!restored.ok) {
|
|
292
301
|
errors.push(`STOP: cannot restore ${entry.path}; prior payload staged at ${path.join(entry.dir.path, rName)} for manual recovery`);
|