From 55d60917b0e0e97b4d6b4988ae136c4eaf3577bc Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Thu, 17 Sep 2026 14:32:24 +0100 Subject: [PATCH 1/8] fix(deps): clear all Dependabot alerts in the Defender plugins Both Defender plugins pinned adm-zip and sharp through `overrides`, and both pins had themselves become the vulnerable versions. sharp 0.35.0 -> 0.35.4 (GHSA-rgj7-g3m4-5g8c, high). Pulled in by @huggingface/transformers. 0.35.4 is the first patched release. adm-zip 0.6.0 -> 0.6.1 (GHSA-vwc7-r8mq-g2x9, medium). Pulled in by onnxruntime-node. The advisory lists no patched version because it predates the release, but 0.6.1 sits outside the affected range (>= 0.5.9, <= 0.6.0) and carries the commit that fixes it: "Blocked extraction from writing through symlinks inside the target". Verified: both plugins install cleanly, `npm audit` reports zero vulnerabilities in each, and the suites pass (15 tests for stackone-defender, 12 for stackone-defender-antigravity). The lockfile churn is confined to adm-zip, sharp and sharp's platform binaries. Co-Authored-By: Claude Opus 5 --- .../package-lock.json | 251 ++++++++--------- .../package.json | 4 +- .../stackone-defender/package-lock.json | 257 +++++++++--------- .../security/stackone-defender/package.json | 4 +- 4 files changed, 262 insertions(+), 254 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/package-lock.json b/plugins/security/stackone-defender-antigravity/package-lock.json index c98bdb8..910231e 100644 --- a/plugins/security/stackone-defender-antigravity/package-lock.json +++ b/plugins/security/stackone-defender-antigravity/package-lock.json @@ -138,9 +138,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.0.tgz", - "integrity": "sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -156,13 +156,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.0" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.0.tgz", - "integrity": "sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -178,20 +178,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.0" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.0.tgz", - "integrity": "sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -201,9 +201,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", - "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", - "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", - "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -249,9 +249,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", - "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -265,9 +265,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", - "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -281,9 +281,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", - "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -297,9 +297,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", - "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -313,9 +313,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", - "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -329,9 +329,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", - "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", - "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.0.tgz", - "integrity": "sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -379,13 +379,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.0" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.0.tgz", - "integrity": "sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -401,13 +401,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.0" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.0.tgz", - "integrity": "sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -423,13 +423,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.0" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.0.tgz", - "integrity": "sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -445,13 +445,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.0" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.0.tgz", - "integrity": "sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -467,13 +467,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.0" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.0.tgz", - "integrity": "sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -489,13 +489,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.0" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.0.tgz", - "integrity": "sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -511,13 +511,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.0.tgz", - "integrity": "sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -533,17 +533,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.0.tgz", - "integrity": "sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.0" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -553,16 +553,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.0.tgz", - "integrity": "sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -572,9 +572,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.0.tgz", - "integrity": "sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -591,9 +591,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.0.tgz", - "integrity": "sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -610,9 +610,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.0.tgz", - "integrity": "sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -732,9 +732,9 @@ } }, "node_modules/adm-zip": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", - "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { "node": ">=14.0" @@ -1115,14 +1115,14 @@ } }, "node_modules/sharp": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.0.tgz", - "integrity": "sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "semver": "^7.8.5" }, "engines": { "node": ">=20.9.0" @@ -1131,31 +1131,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.0", - "@img/sharp-darwin-x64": "0.35.0", - "@img/sharp-freebsd-wasm32": "0.35.0", - "@img/sharp-libvips-darwin-arm64": "1.3.0", - "@img/sharp-libvips-darwin-x64": "1.3.0", - "@img/sharp-libvips-linux-arm": "1.3.0", - "@img/sharp-libvips-linux-arm64": "1.3.0", - "@img/sharp-libvips-linux-ppc64": "1.3.0", - "@img/sharp-libvips-linux-riscv64": "1.3.0", - "@img/sharp-libvips-linux-s390x": "1.3.0", - "@img/sharp-libvips-linux-x64": "1.3.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", - "@img/sharp-libvips-linuxmusl-x64": "1.3.0", - "@img/sharp-linux-arm": "0.35.0", - "@img/sharp-linux-arm64": "0.35.0", - "@img/sharp-linux-ppc64": "0.35.0", - "@img/sharp-linux-riscv64": "0.35.0", - "@img/sharp-linux-s390x": "0.35.0", - "@img/sharp-linux-x64": "0.35.0", - "@img/sharp-linuxmusl-arm64": "0.35.0", - "@img/sharp-linuxmusl-x64": "0.35.0", - "@img/sharp-webcontainers-wasm32": "0.35.0", - "@img/sharp-win32-arm64": "0.35.0", - "@img/sharp-win32-ia32": "0.35.0", - "@img/sharp-win32-x64": "0.35.0" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/sprintf-js": { diff --git a/plugins/security/stackone-defender-antigravity/package.json b/plugins/security/stackone-defender-antigravity/package.json index faf098f..c4e6677 100644 --- a/plugins/security/stackone-defender-antigravity/package.json +++ b/plugins/security/stackone-defender-antigravity/package.json @@ -22,10 +22,10 @@ "onnxruntime-node": "^1.24.3" }, "overrides": { - "adm-zip": "0.6.0", + "adm-zip": "0.6.1", "nanoid": "3.3.18", "protobufjs": "7.6.5", - "sharp": "0.35.0", + "sharp": "0.35.4", "tar": "7.5.21" } } diff --git a/plugins/security/stackone-defender/package-lock.json b/plugins/security/stackone-defender/package-lock.json index 0fe5eb5..cce4537 100644 --- a/plugins/security/stackone-defender/package-lock.json +++ b/plugins/security/stackone-defender/package-lock.json @@ -1,12 +1,12 @@ { "name": "stackone-defender", - "version": "3.0.0", + "version": "3.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "stackone-defender", - "version": "3.0.0", + "version": "3.2.0", "license": "MIT", "dependencies": { "@huggingface/transformers": "^3.8.1", @@ -138,9 +138,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.0.tgz", - "integrity": "sha512-ZgaYEwaj+lx/5n4W8GmZ2IYz0PQHjN5eqRcfijWGB+2Aq7ZInZGa0qJyAn6DEtyLuWHRSrmWOqT9q3qqTBvmUQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", "cpu": [ "arm64" ], @@ -156,13 +156,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.3.0" + "@img/sharp-libvips-darwin-arm64": "1.3.3" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.0.tgz", - "integrity": "sha512-c1z9LFpKB0slQW3RchwBE8iSVzGp70TNjUUO9k4BZwwW4HH7JBGHeIy4b+kk4n/kcBASb9evKCE3/7Slmslgiw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", "cpu": [ "x64" ], @@ -178,20 +178,20 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.3.0" + "@img/sharp-libvips-darwin-x64": "1.3.3" } }, "node_modules/@img/sharp-freebsd-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.0.tgz", - "integrity": "sha512-Li2KTev0H90kEtnJHkI9xQojXt1AqWmFBMXiPw5kqd1jQgP7gi5HVK/qC5Rmh/59NuAwUuPzzPITmX22NomYYQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", "license": "Apache-2.0", "optional": true, "os": [ "freebsd" ], "dependencies": { - "@img/sharp-wasm32": "0.35.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -201,9 +201,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.0.tgz", - "integrity": "sha512-EKbmBKtyTH+GPFDRw2TgK2oV6hyxxlJVIar4hoTYSNmIwipgMFdxPQqR392GmfdsPGWga0mCFN1cCKjRb9cljw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", "cpu": [ "arm64" ], @@ -217,9 +217,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.0.tgz", - "integrity": "sha512-Pl2OmOvrJ42adUllESxBsG54PfXLo1OYg9i3c5/5Ln/qJ0gZuTM9YMhQJPIbXqwidLRc/c2zuHt4RsrymmNv7A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", "cpu": [ "x64" ], @@ -233,9 +233,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.0.tgz", - "integrity": "sha512-A8UpHoUDW4DwnXoV6+q3C1s7QLRAHtPDEjWuNZjwHMyoCNZnm0GeNN8ls9f/bsEYTRQRW96C/n34XJQHJ2fT7A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", "cpu": [ "arm" ], @@ -249,9 +249,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.0.tgz", - "integrity": "sha512-C0SqjoFKnszqa44EQ7xoaT48nnO0lOyXEULfXMWi8krrjOPGYkeK30Okzla6ATbBYsyZ0ySinK0FVkpv3DwzfQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", "cpu": [ "arm64" ], @@ -265,9 +265,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.0.tgz", - "integrity": "sha512-WOpkVxAjFd369iaIzEgNRreFD+gWdUMIGD5zplhNKNeqS6mm5dac3q2AFyCBmzYoAdouzZvRBgxy4z8QHZb4/A==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", "cpu": [ "ppc64" ], @@ -281,9 +281,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.0.tgz", - "integrity": "sha512-DRWw0mOHusrCCuw2rqP87oLg6PGlkomVDFqw2hIwsSfwWpu4k3XLcBPaKKl6ct/GtL/cwNkgwjV/tc0Mqht3VA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", "cpu": [ "riscv64" ], @@ -297,9 +297,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.0.tgz", - "integrity": "sha512-9APy+nFWhHS+kzLgWZfLcyrUd7YqnAQVa4BPOo4xkoHpdoktOAPG4cEr9+Jpl0TtqfVmcMJimNL5qNTyyOHZNA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", "cpu": [ "s390x" ], @@ -313,9 +313,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.0.tgz", - "integrity": "sha512-y9RNUYDe2A1UAdhLyfeOodGRszQdaEoe4nfOpp/sNVPl2CWIcUyFaDoCh4vPLPxu19803j2naLqZup2WxDXCLA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", "cpu": [ "x64" ], @@ -329,9 +329,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.0.tgz", - "integrity": "sha512-cC1wkC0Mlucd0KSiGrLkJnB/ZqPvZCntc/Lk7ZnYO5ZSbF2euNek4Xvxafojq+wN1q/W0eprdpUIjUr/EV2PBg==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", "cpu": [ "arm64" ], @@ -345,9 +345,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.0.tgz", - "integrity": "sha512-LiYMhUZicB1QG//+RvmYZpXJO8fYRENfp+MZUCnG9aw+AKvGAy9gPaCnuwsPcBFs8EV66M0NNxj9VHcNklE8zw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", "cpu": [ "x64" ], @@ -361,9 +361,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.0.tgz", - "integrity": "sha512-VVlpEWwizEFIOom0zdoeKuO5nuTswzVE5uHcBNvHzmeHUpNFajY3HFfbQ+zIH4E2kVaZ/yVxmsShW56TtEy4uA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", "cpu": [ "arm" ], @@ -379,13 +379,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.3.0" + "@img/sharp-libvips-linux-arm": "1.3.3" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.0.tgz", - "integrity": "sha512-4+4XHLNT5wDT0roYlHTEmH9lDKt0acf9Tv+3hM3iceOirkxrR404/3WjAYZ9F9CkHrxeRcGLJXbi4vluMZ9O+A==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", "cpu": [ "arm64" ], @@ -401,13 +401,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.3.0" + "@img/sharp-libvips-linux-arm64": "1.3.3" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.0.tgz", - "integrity": "sha512-N3hzbEpUTJC8pWpPVJvgzGxM+so/MAXc8O2s/53B0LL9ZGpfXpME7Wizkc5d/8fRBlBtkDjzoZGDCqqNDHqLEw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", "cpu": [ "ppc64" ], @@ -423,13 +423,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.3.0" + "@img/sharp-libvips-linux-ppc64": "1.3.3" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.0.tgz", - "integrity": "sha512-l6vmKVPnbS0RhVMbyxP5meAARsbhCnBN4fy31qz0+3a6Rv4jEqfzDrT89y6ZPkCi0AJGnwp2En528yXo401Hpw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", "cpu": [ "riscv64" ], @@ -445,13 +445,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.3.0" + "@img/sharp-libvips-linux-riscv64": "1.3.3" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.0.tgz", - "integrity": "sha512-MYlMiPFiv/EKPAHnp3yNZ9AAWFsxga9c5Bkc6wkar6bqzHLlkGVJHRm0u1ei+VXnZxp3Mz9MG9ZIsI8vSOf3sQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", "cpu": [ "s390x" ], @@ -467,13 +467,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.3.0" + "@img/sharp-libvips-linux-s390x": "1.3.3" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.0.tgz", - "integrity": "sha512-TYaItB5oj1ioXjhyn2xrR208vf+YuIIcHptQWRRaBmFhvIvL9D72DXN8w75xup0KXA8UdEAhQ9Qb2S49FD/9Cw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", "cpu": [ "x64" ], @@ -489,13 +489,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.3.0" + "@img/sharp-libvips-linux-x64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.0.tgz", - "integrity": "sha512-DSTb6ijQzqe6DdAaOBVqJ/SYf1vO8EW5bK6X6LRXufEBebf2722VCdvBUtZ3rtV0x2ApfPNDy/p7LrrjaWjiyQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", "cpu": [ "arm64" ], @@ -511,13 +511,13 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.3.0" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.0.tgz", - "integrity": "sha512-K7ykQ+26Rt6+4BTU80AuGgTPIYX86UxiAKT4rcXX/WNTo7k1ZxpKz+TguHnwVpCqQK3B5PK0vZ0ZBe6nz/ib1w==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", "cpu": [ "x64" ], @@ -533,17 +533,17 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.3.0" + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.0.tgz", - "integrity": "sha512-9woLIFORERCr+6cWu87dQ22J34EExkhc73U1kZW0c+RclQqWetoodByp4dWZ/hN8/KVmTRAx2HOnUwib8AwZdA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.11.0" + "@emnapi/runtime": "^1.11.3" }, "engines": { "node": ">=20.9.0" @@ -553,16 +553,16 @@ } }, "node_modules/@img/sharp-webcontainers-wasm32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.0.tgz", - "integrity": "sha512-t+kie1TOyaDM6Dho+f+y0VqIUNhYQaKCUahuZVi0E0frgdiaOaPsDxDW3wfKacUdaNBCnK/ZDBMg33ydvHj8uA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", "cpu": [ "wasm32" ], "license": "Apache-2.0", "optional": true, "dependencies": { - "@img/sharp-wasm32": "0.35.0" + "@img/sharp-wasm32": "0.35.4" }, "engines": { "node": ">=20.9.0" @@ -572,9 +572,9 @@ } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.0.tgz", - "integrity": "sha512-M5eKxug0dabbaWgFKvPa3odNs2OpaP+81NASfGKkt4GcYXpNhSu7CaeYxWkLNV6vHmUp4hnCxnxrUyhUJhXbKA==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", "cpu": [ "arm64" ], @@ -591,9 +591,9 @@ } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.0.tgz", - "integrity": "sha512-z0+pZ03QCDvdVN0Ez9IX/yjWC19ikMlXrmdYMwYNLTh2BLPx3hXWPvyqWfquZ0BTO9O6GVOjIVoTcyyacMnWlQ==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", "cpu": [ "ia32" ], @@ -610,9 +610,9 @@ } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.0.tgz", - "integrity": "sha512-feNnlz5ZHKr0MY1LPHvZQyJeBkbo4ctsn0D8FvA53VTw5TC63rfEL2UrWbkSBR19htSE7Mw78xYVwdJqoMWVHw==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", "cpu": [ "x64" ], @@ -732,9 +732,9 @@ } }, "node_modules/adm-zip": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.0.tgz", - "integrity": "sha512-XleryMhbuksdKtofnWZ9Sk+4CUTbms4Mb/EU32SZwToAyZ5RgVos/ki8n+yr0LWHOGKuakbXTuuYNHLQjhddgg==", + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.6.1.tgz", + "integrity": "sha512-Xwrja8nx9e5o2N1my4DsKCeKpdrnACyr1wtbPxBDgGzKzKyE9kRtBFA8mWldI+RVlD7CBZNWY/wQ2+ydwOR6kQ==", "license": "MIT", "engines": { "node": ">=14.0" @@ -846,7 +846,6 @@ "resolved": "https://registry.npmjs.org/fasttext.wasm/-/fasttext.wasm-1.0.1.tgz", "integrity": "sha512-9a3ton6jy+y4sqJOahv62gMploUVDh8H+BlG1HkRyHHLGLLKHjIQNoi6JnUxMH1mi6cj3LToT9Tl56ajkJ9xnQ==", "license": "MIT", - "peer": true, "engines": { "node": ">=14.15.0" } @@ -1010,7 +1009,6 @@ "darwin", "linux" ], - "peer": true, "dependencies": { "adm-zip": "^0.5.16", "global-agent": "^4.1.3", @@ -1117,14 +1115,14 @@ } }, "node_modules/sharp": { - "version": "0.35.0", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.0.tgz", - "integrity": "sha512-BqvG5XbwPZ4NV0DK90d86leEECMsoa8bO0nqnKWlBDYxri4GJ7c4EDInaF6q20lTh/mATmnDIKWJFfXnoVfH5g==", + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", "license": "Apache-2.0", "dependencies": { "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.8.4" + "semver": "^7.8.5" }, "engines": { "node": ">=20.9.0" @@ -1133,31 +1131,36 @@ "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.35.0", - "@img/sharp-darwin-x64": "0.35.0", - "@img/sharp-freebsd-wasm32": "0.35.0", - "@img/sharp-libvips-darwin-arm64": "1.3.0", - "@img/sharp-libvips-darwin-x64": "1.3.0", - "@img/sharp-libvips-linux-arm": "1.3.0", - "@img/sharp-libvips-linux-arm64": "1.3.0", - "@img/sharp-libvips-linux-ppc64": "1.3.0", - "@img/sharp-libvips-linux-riscv64": "1.3.0", - "@img/sharp-libvips-linux-s390x": "1.3.0", - "@img/sharp-libvips-linux-x64": "1.3.0", - "@img/sharp-libvips-linuxmusl-arm64": "1.3.0", - "@img/sharp-libvips-linuxmusl-x64": "1.3.0", - "@img/sharp-linux-arm": "0.35.0", - "@img/sharp-linux-arm64": "0.35.0", - "@img/sharp-linux-ppc64": "0.35.0", - "@img/sharp-linux-riscv64": "0.35.0", - "@img/sharp-linux-s390x": "0.35.0", - "@img/sharp-linux-x64": "0.35.0", - "@img/sharp-linuxmusl-arm64": "0.35.0", - "@img/sharp-linuxmusl-x64": "0.35.0", - "@img/sharp-webcontainers-wasm32": "0.35.0", - "@img/sharp-win32-arm64": "0.35.0", - "@img/sharp-win32-ia32": "0.35.0", - "@img/sharp-win32-x64": "0.35.0" + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/sprintf-js": { diff --git a/plugins/security/stackone-defender/package.json b/plugins/security/stackone-defender/package.json index 5524beb..5c7eacc 100644 --- a/plugins/security/stackone-defender/package.json +++ b/plugins/security/stackone-defender/package.json @@ -22,10 +22,10 @@ "onnxruntime-node": "^1.24.3" }, "overrides": { - "adm-zip": "0.6.0", + "adm-zip": "0.6.1", "nanoid": "3.3.18", "protobufjs": "7.6.5", - "sharp": "0.35.0", + "sharp": "0.35.4", "tar": "7.5.21" } } From 1c5501707bf914ba1e3a46f7b71f91b5de31a47a Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 09:28:34 +0100 Subject: [PATCH 2/8] fix(defender): reinstall dependencies when pins change on upgrade `ensureDepsInstalled()` only checked that each direct dependency folder existed, so an install that already had node_modules never ran `npm install` again. Security pins for transitive packages live in `overrides`, which means a plugin upgrade that moves a pin left existing users on the old, vulnerable version. That would have made the adm-zip and sharp bumps in this PR reach new installs only. The hook now stamps a fingerprint of `dependencies` plus `overrides` into node_modules after each install, and reinstalls when the fingerprint moves. Steady state still skips the install, so there is no cost in the hot path. An unreadable package.json falls back to the old presence check rather than reinstalling on every invocation. Verified across the three states: an existing install with no stamp reinstalls, a stamped install skips, and changing an override pin triggers a reinstall. Both suites still pass, 15 and 12 tests. Co-Authored-By: Claude Opus 5 --- .../scripts/scan-tool-result.mjs | 40 ++++++++++++++++++- .../scripts/scan-tool-result.mjs | 40 ++++++++++++++++++- 2 files changed, 78 insertions(+), 2 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index f725748..b0e2fbc 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -39,13 +39,16 @@ import { closeSync, unlinkSync, statSync, + writeFileSync, } from "fs"; import { execSync, spawn } from "child_process"; +import { createHash } from "crypto"; import net from "net"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = join(scriptDir, ".."); const DAEMON_SCRIPT = join(scriptDir, "defender-daemon.mjs"); +const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); const SOCKET_PATH = join(homedir(), ".claude", "defender.sock"); const LOCK_PATH = join(homedir(), ".claude", "defender-daemon.lock"); const STATE_PATH = join(homedir(), ".claude", "defender-daemon.json"); @@ -91,14 +94,49 @@ function readPluginDeps() { } } +// Fingerprint of everything that decides which versions end up in node_modules. +// `overrides` matters as much as `dependencies` here: security pins for transitive +// packages live there, and a plugin upgrade that only moves a pin would otherwise +// leave an existing install on the old, vulnerable version. +function depsFingerprint() { + try { + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + return createHash("sha256") + .update(JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} })) + .digest("hex"); + } catch { + return null; + } +} + +function readDepsStamp() { + try { + return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + } catch { + return null; + } +} + function ensureDepsInstalled() { const deps = readPluginDeps(); const missing = deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))); - if (!missing) return true; + const fingerprint = depsFingerprint(); + // Reinstall when a direct dependency is absent, or when dependencies/overrides have + // moved since the last install. A null fingerprint means package.json is unreadable, + // in which case fall back to the presence check alone rather than reinstalling forever. + const stale = fingerprint !== null && readDepsStamp() !== fingerprint; + if (!missing && !stale) return true; try { execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { timeout: 120_000, }); + if (fingerprint !== null) { + try { + writeFileSync(DEPS_STAMP_PATH, fingerprint); + } catch { + // A missing stamp only costs a redundant install next run. + } + } return true; } catch (err) { process.stderr.write(`[Defender] Dependency install failed — scanner disabled: ${err.message}\n`); diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index e6441b2..cc40c34 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -20,13 +20,16 @@ import { closeSync, unlinkSync, statSync, + writeFileSync, } from "fs"; import { execSync, spawn } from "child_process"; +import { createHash } from "crypto"; import net from "net"; const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = join(scriptDir, ".."); const DAEMON_SCRIPT = join(scriptDir, "defender-daemon.mjs"); +const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); const SOCKET_PATH = join(homedir(), ".claude", "defender.sock"); const LOCK_PATH = join(homedir(), ".claude", "defender-daemon.lock"); const STATE_PATH = join(homedir(), ".claude", "defender-daemon.json"); @@ -78,14 +81,49 @@ function readPluginDeps() { } } +// Fingerprint of everything that decides which versions end up in node_modules. +// `overrides` matters as much as `dependencies` here: security pins for transitive +// packages live there, and a plugin upgrade that only moves a pin would otherwise +// leave an existing install on the old, vulnerable version. +function depsFingerprint() { + try { + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + return createHash("sha256") + .update(JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} })) + .digest("hex"); + } catch { + return null; + } +} + +function readDepsStamp() { + try { + return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + } catch { + return null; + } +} + function ensureDepsInstalled() { const deps = readPluginDeps(); const missing = deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))); - if (!missing) return true; + const fingerprint = depsFingerprint(); + // Reinstall when a direct dependency is absent, or when dependencies/overrides have + // moved since the last install. A null fingerprint means package.json is unreadable, + // in which case fall back to the presence check alone rather than reinstalling forever. + const stale = fingerprint !== null && readDepsStamp() !== fingerprint; + if (!missing && !stale) return true; try { execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { timeout: 120_000, }); + if (fingerprint !== null) { + try { + writeFileSync(DEPS_STAMP_PATH, fingerprint); + } catch { + // A missing stamp only costs a redundant install next run. + } + } return true; } catch (err) { process.stderr.write(`[Defender] Dependency install failed — scanner disabled: ${err.message}\n`); From b83ce9fd2225db824462729bb16bbb396df69cd1 Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 09:36:32 +0100 Subject: [PATCH 3/8] fix(defender): hash the lockfile and restart the daemon on pin changes Two gaps in the previous commit, both raised in review. The fingerprint covered only `dependencies` and `overrides`, so a lockfile-only change (a transitive bump needing no override) left it unmoved and skipped the reinstall. npm resolves from the lockfile when one is present, so its contents now feed the hash too. The daemon loads the plugin's dependency tree into its own process, and the client only compared `defenderVersion`, which does not move when an override does. An already-running daemon therefore kept serving scans from the old tree after an upgrade. The daemon now records the dependency stamp in its state file and the client replaces any daemon whose stamp differs. Both read the same stamp, so there is one source of truth and no duplicated hashing. Verified: a lockfile-only edit changes the fingerprint; a daemon with an old or absent stamp is replaced; a matching stamp is reused; and no stamp on disk means reuse, so there is no restart loop on a first run. Both suites pass, 15 and 12. Co-Authored-By: Claude Opus 5 --- .../scripts/defender-daemon.mjs | 13 +++++++++++ .../scripts/scan-tool-result.mjs | 23 ++++++++++++++++--- .../scripts/defender-daemon.mjs | 13 +++++++++++ .../scripts/scan-tool-result.mjs | 20 +++++++++++++--- 4 files changed, 63 insertions(+), 6 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs index 6e86950..9f4cc7f 100644 --- a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs @@ -25,6 +25,16 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); +const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); + +function readDepsStamp() { + try { + return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + } catch { + return null; + } +} + const requireFrom = createRequire(join(pluginRoot, "package.json")); function rotateLogIfNeeded() { @@ -275,6 +285,9 @@ server.listen(SOCKET_PATH, () => { const state = { pid: process.pid, defenderVersion, + // Same stamp the client writes after an install. Recording it lets the client + // tell that this daemon predates a dependency change and needs replacing. + depsStamp: readDepsStamp(), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index b0e2fbc..e5fd041 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -101,9 +101,17 @@ function readPluginDeps() { function depsFingerprint() { try { const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - return createHash("sha256") - .update(JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} })) - .digest("hex"); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); } catch { return null; } @@ -249,6 +257,7 @@ function waitForSocket(deadline) { async function ensureDaemonRunning() { const expectedVersion = getExpectedDefenderVersion(); + const expectedStamp = readDepsStamp(); const running = getRunningDaemonInfo(); if (running) { if (!processAlive(running.pid)) { @@ -258,6 +267,14 @@ async function ensureDaemonRunning() { running.pid, `defender version mismatch: running=${running.defenderVersion} expected=${expectedVersion}`, ); + } else if (expectedStamp && running.depsStamp !== expectedStamp) { + // The daemon loads the plugin's dependency tree into its own process, so new + // pins only take effect once it restarts. `defenderVersion` does not move when + // an override does, which would otherwise leave the old tree serving scans. + await killAndClean( + running.pid, + `dependency fingerprint mismatch: running=${running.depsStamp ?? "none"} expected=${expectedStamp}`, + ); } } else if (existsSync(SOCKET_PATH)) { try { diff --git a/plugins/security/stackone-defender/scripts/defender-daemon.mjs b/plugins/security/stackone-defender/scripts/defender-daemon.mjs index 6e86950..9f4cc7f 100644 --- a/plugins/security/stackone-defender/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender/scripts/defender-daemon.mjs @@ -25,6 +25,16 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); +const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); + +function readDepsStamp() { + try { + return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + } catch { + return null; + } +} + const requireFrom = createRequire(join(pluginRoot, "package.json")); function rotateLogIfNeeded() { @@ -275,6 +285,9 @@ server.listen(SOCKET_PATH, () => { const state = { pid: process.pid, defenderVersion, + // Same stamp the client writes after an install. Recording it lets the client + // tell that this daemon predates a dependency change and needs replacing. + depsStamp: readDepsStamp(), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index cc40c34..18cf38a 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -88,9 +88,17 @@ function readPluginDeps() { function depsFingerprint() { try { const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - return createHash("sha256") - .update(JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} })) - .digest("hex"); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); } catch { return null; } @@ -239,12 +247,18 @@ async function ensureDaemonRunning() { // alive AND matches the defender version currently in node_modules. // Either mismatch counts as "needs respawn" — same code path as cold. const expectedVersion = getExpectedDefenderVersion(); + const expectedStamp = readDepsStamp(); const running = getRunningDaemonInfo(); if (running) { if (!processAlive(running.pid)) { await killAndClean(running.pid, "stale state file — pid not alive"); } else if (expectedVersion && running.defenderVersion !== expectedVersion) { await killAndClean(running.pid, `defender version mismatch: running=${running.defenderVersion} expected=${expectedVersion}`); + } else if (expectedStamp && running.depsStamp !== expectedStamp) { + // The daemon loads the plugin's dependency tree into its own process, so new + // pins only take effect once it restarts. `defenderVersion` does not move when + // an override does, which would otherwise leave the old tree serving scans. + await killAndClean(running.pid, `dependency fingerprint mismatch: running=${running.depsStamp ?? "none"} expected=${expectedStamp}`); } } else if (existsSync(SOCKET_PATH)) { // Socket without state file — crash recovery. Clean it. From 143e59c3037e58019ab6954ffd77ae03d7edc72c Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 09:44:10 +0100 Subject: [PATCH 4/8] fix(defender): validate the daemon against a computed fingerprint The client read the expected fingerprint back from the stamp file, so a failed stamp write left it null and silently disabled daemon validation: the old dependency tree kept serving scans while every later hook retried npm install. Both sides now compute the fingerprint from package.json and the lockfile instead of exchanging it through a file. The stamp file goes back to its one job, recording what was last installed, and a write failure there costs at most a redundant install rather than a daemon that never gets replaced. Passing the computed value only on the client was not enough: the daemon would have kept reporting a stamp it read from disk, and a missing file would have produced an endless restart loop. Verified: a failed stamp write now still replaces a stale daemon, a daemon on the same fingerprint is reused, a legacy daemon with no field is replaced, and an unreadable package.json reuses rather than looping. Both suites pass. Co-Authored-By: Claude Opus 5 --- .../scripts/defender-daemon.mjs | 21 ++++++++++++++----- .../scripts/scan-tool-result.mjs | 4 +++- .../scripts/defender-daemon.mjs | 21 ++++++++++++++----- .../scripts/scan-tool-result.mjs | 4 +++- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs index 9f4cc7f..f267871 100644 --- a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs @@ -12,6 +12,7 @@ import { dirname, join, resolve } from "path"; import { homedir } from "os"; import { fileURLToPath } from "url"; import { createServer } from "net"; +import { createHash } from "crypto"; import { unlinkSync, existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync, renameSync } from "fs"; const PROTOCOL_VERSION = 1; @@ -25,11 +26,21 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); -const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); - -function readDepsStamp() { +// Must stay in step with the same function in scan-tool-result.mjs. +function depsFingerprint() { try { - return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); } catch { return null; } @@ -287,7 +298,7 @@ server.listen(SOCKET_PATH, () => { defenderVersion, // Same stamp the client writes after an install. Recording it lets the client // tell that this daemon predates a dependency change and needs replacing. - depsStamp: readDepsStamp(), + depsStamp: depsFingerprint(), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index e5fd041..76f1e84 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -257,7 +257,9 @@ function waitForSocket(deadline) { async function ensureDaemonRunning() { const expectedVersion = getExpectedDefenderVersion(); - const expectedStamp = readDepsStamp(); + // Computed, not read back from the stamp file: a failed stamp write must not + // quietly disable daemon validation and leave the old tree serving scans. + const expectedStamp = depsFingerprint(); const running = getRunningDaemonInfo(); if (running) { if (!processAlive(running.pid)) { diff --git a/plugins/security/stackone-defender/scripts/defender-daemon.mjs b/plugins/security/stackone-defender/scripts/defender-daemon.mjs index 9f4cc7f..f267871 100644 --- a/plugins/security/stackone-defender/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender/scripts/defender-daemon.mjs @@ -12,6 +12,7 @@ import { dirname, join, resolve } from "path"; import { homedir } from "os"; import { fileURLToPath } from "url"; import { createServer } from "net"; +import { createHash } from "crypto"; import { unlinkSync, existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync, renameSync } from "fs"; const PROTOCOL_VERSION = 1; @@ -25,11 +26,21 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); -const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); - -function readDepsStamp() { +// Must stay in step with the same function in scan-tool-result.mjs. +function depsFingerprint() { try { - return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); } catch { return null; } @@ -287,7 +298,7 @@ server.listen(SOCKET_PATH, () => { defenderVersion, // Same stamp the client writes after an install. Recording it lets the client // tell that this daemon predates a dependency change and needs replacing. - depsStamp: readDepsStamp(), + depsStamp: depsFingerprint(), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index 18cf38a..b9fe1ab 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -247,7 +247,9 @@ async function ensureDaemonRunning() { // alive AND matches the defender version currently in node_modules. // Either mismatch counts as "needs respawn" — same code path as cold. const expectedVersion = getExpectedDefenderVersion(); - const expectedStamp = readDepsStamp(); + // Computed, not read back from the stamp file: a failed stamp write must not + // quietly disable daemon validation and leave the old tree serving scans. + const expectedStamp = depsFingerprint(); const running = getRunningDaemonInfo(); if (running) { if (!processAlive(running.pid)) { From 1a941e0f12759905382acbae559325d2a7cba9e6 Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 09:51:06 +0100 Subject: [PATCH 5/8] fix(defender): lock dependency installs and stamp the post-install state Two more issues from review, both introduced by the stale-stamp path itself. There was no lock around `npm install`. Previously the install ran only when a direct dependency was missing, so it was effectively a cold-start path. Now every concurrent PostToolUse hook sees the same stale stamp after an upgrade and would run npm against one prefix at once. Installs take a per-plugin lock and recheck the stamp while holding it; a hook that loses the race scans with the tree it has rather than blocking the tool result. The stamp also recorded the fingerprint computed before the install, and the lockfile feeds that hash. npm normalises the lockfile as it installs, so the stored value could disagree with the tree it described and trigger a pointless reinstall on the next hook. The stamp is now computed after npm returns. The lock is per-plugin, so the two Defender variants never serialise against each other, and it is gitignored since a crash can leave it behind. Both suites pass, 15 and 12. Co-Authored-By: Claude Opus 5 --- .gitignore | 2 +- .../scripts/scan-tool-result.mjs | 49 ++++++++++++++++--- .../scripts/scan-tool-result.mjs | 49 ++++++++++++++++--- 3 files changed, 83 insertions(+), 17 deletions(-) diff --git a/.gitignore b/.gitignore index d79b35e..0621e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,2 @@ node_modules/ -node_modules/ +.stackone-deps-install.lock diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index 76f1e84..f50b10a 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -49,6 +49,8 @@ const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = join(scriptDir, ".."); const DAEMON_SCRIPT = join(scriptDir, "defender-daemon.mjs"); const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); +// Per-plugin, so the two Defender variants never serialise against each other. +const DEPS_LOCK_PATH = join(pluginRoot, ".stackone-deps-install.lock"); const SOCKET_PATH = join(homedir(), ".claude", "defender.sock"); const LOCK_PATH = join(homedir(), ".claude", "defender-daemon.lock"); const STATE_PATH = join(homedir(), ".claude", "defender-daemon.json"); @@ -125,22 +127,46 @@ function readDepsStamp() { } } +function depsUpToDate(missing) { + if (missing) return false; + const fingerprint = depsFingerprint(); + // A null fingerprint means package.json is unreadable. Fall back to the presence + // check alone rather than reinstalling on every invocation. + return fingerprint === null || readDepsStamp() === fingerprint; +} + function ensureDepsInstalled() { const deps = readPluginDeps(); const missing = deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))); - const fingerprint = depsFingerprint(); - // Reinstall when a direct dependency is absent, or when dependencies/overrides have - // moved since the last install. A null fingerprint means package.json is unreadable, - // in which case fall back to the presence check alone rather than reinstalling forever. - const stale = fingerprint !== null && readDepsStamp() !== fingerprint; - if (!missing && !stale) return true; + if (depsUpToDate(missing)) return true; + + // Serialise installs. After an upgrade every concurrent hook sees the same stale + // stamp, and npm is not safe to run against one prefix from several processes. + let lockFd = null; try { + lockFd = openSync(DEPS_LOCK_PATH, "wx"); + } catch (err) { + if (err.code === "EEXIST") { + // Another hook is installing. Scan with the tree we have rather than block the + // tool result; the next invocation picks up the refreshed one. + return !missing; + } + process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); + return false; + } + + try { + // Recheck under the lock: whoever held it first may have finished the install. + if (depsUpToDate(deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))))) return true; execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { timeout: 120_000, }); - if (fingerprint !== null) { + // Recompute after the install: npm normalises the lockfile, and the lockfile feeds + // the hash, so stamping the pre-install value would look stale on the next run. + const installed = depsFingerprint(); + if (installed !== null) { try { - writeFileSync(DEPS_STAMP_PATH, fingerprint); + writeFileSync(DEPS_STAMP_PATH, installed); } catch { // A missing stamp only costs a redundant install next run. } @@ -149,6 +175,13 @@ function ensureDepsInstalled() { } catch (err) { process.stderr.write(`[Defender] Dependency install failed — scanner disabled: ${err.message}\n`); return false; + } finally { + closeSync(lockFd); + try { + unlinkSync(DEPS_LOCK_PATH); + } catch { + // Already removed. + } } } diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index b9fe1ab..4e0f5f6 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -30,6 +30,8 @@ const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = join(scriptDir, ".."); const DAEMON_SCRIPT = join(scriptDir, "defender-daemon.mjs"); const DEPS_STAMP_PATH = join(pluginRoot, "node_modules", ".stackone-deps-stamp"); +// Per-plugin, so the two Defender variants never serialise against each other. +const DEPS_LOCK_PATH = join(pluginRoot, ".stackone-deps-install.lock"); const SOCKET_PATH = join(homedir(), ".claude", "defender.sock"); const LOCK_PATH = join(homedir(), ".claude", "defender-daemon.lock"); const STATE_PATH = join(homedir(), ".claude", "defender-daemon.json"); @@ -112,22 +114,46 @@ function readDepsStamp() { } } +function depsUpToDate(missing) { + if (missing) return false; + const fingerprint = depsFingerprint(); + // A null fingerprint means package.json is unreadable. Fall back to the presence + // check alone rather than reinstalling on every invocation. + return fingerprint === null || readDepsStamp() === fingerprint; +} + function ensureDepsInstalled() { const deps = readPluginDeps(); const missing = deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))); - const fingerprint = depsFingerprint(); - // Reinstall when a direct dependency is absent, or when dependencies/overrides have - // moved since the last install. A null fingerprint means package.json is unreadable, - // in which case fall back to the presence check alone rather than reinstalling forever. - const stale = fingerprint !== null && readDepsStamp() !== fingerprint; - if (!missing && !stale) return true; + if (depsUpToDate(missing)) return true; + + // Serialise installs. After an upgrade every concurrent hook sees the same stale + // stamp, and npm is not safe to run against one prefix from several processes. + let lockFd = null; try { + lockFd = openSync(DEPS_LOCK_PATH, "wx"); + } catch (err) { + if (err.code === "EEXIST") { + // Another hook is installing. Scan with the tree we have rather than block the + // tool result; the next invocation picks up the refreshed one. + return !missing; + } + process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); + return false; + } + + try { + // Recheck under the lock: whoever held it first may have finished the install. + if (depsUpToDate(deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))))) return true; execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { timeout: 120_000, }); - if (fingerprint !== null) { + // Recompute after the install: npm normalises the lockfile, and the lockfile feeds + // the hash, so stamping the pre-install value would look stale on the next run. + const installed = depsFingerprint(); + if (installed !== null) { try { - writeFileSync(DEPS_STAMP_PATH, fingerprint); + writeFileSync(DEPS_STAMP_PATH, installed); } catch { // A missing stamp only costs a redundant install next run. } @@ -136,6 +162,13 @@ function ensureDepsInstalled() { } catch (err) { process.stderr.write(`[Defender] Dependency install failed — scanner disabled: ${err.message}\n`); return false; + } finally { + closeSync(lockFd); + try { + unlinkSync(DEPS_LOCK_PATH); + } catch { + // Already removed. + } } } From f74ca97e0f5f953e01ad5d52d0649ed1ac84d214 Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 09:56:36 +0100 Subject: [PATCH 6/8] fix(defender): reclaim stale install locks and leave the daemon alone mid-install The install lock had no staleness handling. A hook killed between acquiring it and its finally block would leave the file behind, and every later hook would return early without installing. On a plugin whose whole job is keeping a security model current, a lock that silently stops dependency refreshes is the worst of the failure modes. Locks older than 180s, well past npm's own 120s cap, are now reclaimed. Losing the lock also used to fall through into daemon validation. During the owner's install the fingerprint already differs, so the loser could kill the running daemon and spawn a replacement against a half-written node_modules. A hook that loses the lock now leaves daemon lifecycle alone: it scans with whatever is already running, and if nothing is running it skips the event rather than starting a daemon on a partial tree. The next hook starts cleanly. Verified: a free lock is acquired, a live owner blocks a second hook, and a lock left by a crashed owner is reclaimed instead of blocking forever. Both suites pass, 15 and 12. Co-Authored-By: Claude Opus 5 --- .../scripts/scan-tool-result.mjs | 52 ++++++++++++++----- .../scripts/scan-tool-result.mjs | 52 ++++++++++++++----- 2 files changed, 80 insertions(+), 24 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index f50b10a..f4ceb1d 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -127,6 +127,35 @@ function readDepsStamp() { } } +// npm itself is capped at 120s, so a lock older than this belongs to a hook that died +// before its finally block ran. Reclaiming it matters: a lock left behind forever would +// stop dependency refreshes, which is exactly how a vulnerable tree would persist. +const DEPS_LOCK_STALE_MS = 180_000; + +// Set when another hook owns the install. Daemon lifecycle is then off limits: replacing +// a daemon mid-install would let the replacement import a half-written node_modules. +let depsInstallInFlight = false; + +function acquireDepsLock() { + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + return openSync(DEPS_LOCK_PATH, "wx"); + } catch (err) { + if (err.code !== "EEXIST") { + process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); + return null; + } + try { + if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) return null; + unlinkSync(DEPS_LOCK_PATH); + } catch { + // The owner released it between our open and our stat. Try once more. + } + } + } + return null; +} + function depsUpToDate(missing) { if (missing) return false; const fingerprint = depsFingerprint(); @@ -142,17 +171,12 @@ function ensureDepsInstalled() { // Serialise installs. After an upgrade every concurrent hook sees the same stale // stamp, and npm is not safe to run against one prefix from several processes. - let lockFd = null; - try { - lockFd = openSync(DEPS_LOCK_PATH, "wx"); - } catch (err) { - if (err.code === "EEXIST") { - // Another hook is installing. Scan with the tree we have rather than block the - // tool result; the next invocation picks up the refreshed one. - return !missing; - } - process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); - return false; + const lockFd = acquireDepsLock(); + if (lockFd === null) { + // Another hook owns the install. The tree on disk is in flux, so this process must + // not touch the daemon either; scan with what is already running, if anything. + depsInstallInFlight = true; + return !missing; } try { @@ -302,7 +326,7 @@ async function ensureDaemonRunning() { running.pid, `defender version mismatch: running=${running.defenderVersion} expected=${expectedVersion}`, ); - } else if (expectedStamp && running.depsStamp !== expectedStamp) { + } else if (expectedStamp && running.depsStamp !== expectedStamp && !depsInstallInFlight) { // The daemon loads the plugin's dependency tree into its own process, so new // pins only take effect once it restarts. `defenderVersion` does not move when // an override does, which would otherwise leave the old tree serving scans. @@ -321,6 +345,10 @@ async function ensureDaemonRunning() { if (existsSync(SOCKET_PATH)) return true; + // No daemon, and another hook is mid-install. Spawning now would import a + // half-written node_modules, so skip this event; the next hook starts cleanly. + if (depsInstallInFlight) return false; + let lockFd = null; try { lockFd = openSync(LOCK_PATH, "wx"); diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index 4e0f5f6..ef89bba 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -114,6 +114,35 @@ function readDepsStamp() { } } +// npm itself is capped at 120s, so a lock older than this belongs to a hook that died +// before its finally block ran. Reclaiming it matters: a lock left behind forever would +// stop dependency refreshes, which is exactly how a vulnerable tree would persist. +const DEPS_LOCK_STALE_MS = 180_000; + +// Set when another hook owns the install. Daemon lifecycle is then off limits: replacing +// a daemon mid-install would let the replacement import a half-written node_modules. +let depsInstallInFlight = false; + +function acquireDepsLock() { + for (let attempt = 0; attempt < 2; attempt += 1) { + try { + return openSync(DEPS_LOCK_PATH, "wx"); + } catch (err) { + if (err.code !== "EEXIST") { + process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); + return null; + } + try { + if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) return null; + unlinkSync(DEPS_LOCK_PATH); + } catch { + // The owner released it between our open and our stat. Try once more. + } + } + } + return null; +} + function depsUpToDate(missing) { if (missing) return false; const fingerprint = depsFingerprint(); @@ -129,17 +158,12 @@ function ensureDepsInstalled() { // Serialise installs. After an upgrade every concurrent hook sees the same stale // stamp, and npm is not safe to run against one prefix from several processes. - let lockFd = null; - try { - lockFd = openSync(DEPS_LOCK_PATH, "wx"); - } catch (err) { - if (err.code === "EEXIST") { - // Another hook is installing. Scan with the tree we have rather than block the - // tool result; the next invocation picks up the refreshed one. - return !missing; - } - process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); - return false; + const lockFd = acquireDepsLock(); + if (lockFd === null) { + // Another hook owns the install. The tree on disk is in flux, so this process must + // not touch the daemon either; scan with what is already running, if anything. + depsInstallInFlight = true; + return !missing; } try { @@ -289,7 +313,7 @@ async function ensureDaemonRunning() { await killAndClean(running.pid, "stale state file — pid not alive"); } else if (expectedVersion && running.defenderVersion !== expectedVersion) { await killAndClean(running.pid, `defender version mismatch: running=${running.defenderVersion} expected=${expectedVersion}`); - } else if (expectedStamp && running.depsStamp !== expectedStamp) { + } else if (expectedStamp && running.depsStamp !== expectedStamp && !depsInstallInFlight) { // The daemon loads the plugin's dependency tree into its own process, so new // pins only take effect once it restarts. `defenderVersion` does not move when // an override does, which would otherwise leave the old tree serving scans. @@ -306,6 +330,10 @@ async function ensureDaemonRunning() { if (existsSync(SOCKET_PATH)) return true; + // No daemon, and another hook is mid-install. Spawning now would import a + // half-written node_modules, so skip this event; the next hook starts cleanly. + if (depsInstallInFlight) return false; + let lockFd = null; try { lockFd = openSync(LOCK_PATH, "wx"); From f974f819a94bd3a20b157e99dd2e6ed7869faf8f Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Fri, 18 Sep 2026 11:35:28 +0100 Subject: [PATCH 7/8] fix(defender): share the fingerprint module and separate lock errors from contention Two issues cubic raised that I had not picked up, both in code added by this PR. `depsFingerprint()` had been copy-pasted into four files, and the client's value and the daemon's recorded value are compared to decide whether a daemon still matches the tree on disk. Nothing kept the copies in step, so editing one would have produced either a kill/respawn loop on every scan or a permanently stale daemon: precisely the failures the check exists to prevent. It now lives in scripts/deps-fingerprint.mjs, imported by both sides in each plugin. The state file comment claiming the daemon reads the client's stamp was also stale, since round three changed it to compute its own; corrected. `acquireDepsLock()` returned null for both contention and genuine failure, and the caller read every null as contention. A real error such as a permissions problem therefore marked an install in flight, which suppresses daemon replacement and leaves the old tree scanning indefinitely. It now reports the two cases separately: contention scans with the current tree, an error skips the event so the next hook retries. Verified: the shared module is deterministic and the only remaining copy, a free lock is taken, a live owner reports contention, and an unopenable lock path reports an error rather than false contention. Both suites pass, 15 and 12. Co-Authored-By: Claude Opus 5 --- .../scripts/defender-daemon.mjs | 28 ++-------- .../scripts/deps-fingerprint.mjs | 30 +++++++++++ .../scripts/scan-tool-result.mjs | 52 ++++++++----------- .../scripts/defender-daemon.mjs | 28 ++-------- .../scripts/deps-fingerprint.mjs | 30 +++++++++++ .../scripts/scan-tool-result.mjs | 52 ++++++++----------- 6 files changed, 112 insertions(+), 108 deletions(-) create mode 100644 plugins/security/stackone-defender-antigravity/scripts/deps-fingerprint.mjs create mode 100644 plugins/security/stackone-defender/scripts/deps-fingerprint.mjs diff --git a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs index f267871..b4a3787 100644 --- a/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/defender-daemon.mjs @@ -12,7 +12,7 @@ import { dirname, join, resolve } from "path"; import { homedir } from "os"; import { fileURLToPath } from "url"; import { createServer } from "net"; -import { createHash } from "crypto"; +import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; import { unlinkSync, existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync, renameSync } from "fs"; const PROTOCOL_VERSION = 1; @@ -26,26 +26,6 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); -// Must stay in step with the same function in scan-tool-result.mjs. -function depsFingerprint() { - try { - const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - const hash = createHash("sha256").update( - JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), - ); - // npm resolves from the lockfile when one is present, so a lockfile-only change - // (a transitive bump that needed no override) also changes what lands on disk. - try { - hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); - } catch { - // No lockfile: package.json alone decides resolution. - } - return hash.digest("hex"); - } catch { - return null; - } -} - const requireFrom = createRequire(join(pluginRoot, "package.json")); function rotateLogIfNeeded() { @@ -296,9 +276,9 @@ server.listen(SOCKET_PATH, () => { const state = { pid: process.pid, defenderVersion, - // Same stamp the client writes after an install. Recording it lets the client - // tell that this daemon predates a dependency change and needs replacing. - depsStamp: depsFingerprint(), + // Computed here, not read from the client's stamp file, so the client can tell + // that this daemon predates a dependency change and needs replacing. + depsStamp: computeDepsFingerprint(pluginRoot), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender-antigravity/scripts/deps-fingerprint.mjs b/plugins/security/stackone-defender-antigravity/scripts/deps-fingerprint.mjs new file mode 100644 index 0000000..0062f65 --- /dev/null +++ b/plugins/security/stackone-defender-antigravity/scripts/deps-fingerprint.mjs @@ -0,0 +1,30 @@ +/** + * Fingerprint of everything that decides which package versions end up in node_modules. + * + * Imported by both scan-tool-result.mjs and defender-daemon.mjs so the client's expected + * value and the daemon's recorded value can never drift. They are compared to decide + * whether a running daemon still matches the tree on disk, so two copies of this logic + * would mean either a kill/respawn loop on every scan or a permanently stale daemon. + */ +import { readFileSync } from "fs"; +import { join } from "path"; +import { createHash } from "crypto"; + +export function depsFingerprint(pluginRoot) { + try { + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); + } catch { + return null; + } +} diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index f4ceb1d..52c674a 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -42,7 +42,9 @@ import { writeFileSync, } from "fs"; import { execSync, spawn } from "child_process"; -import { createHash } from "crypto"; +import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; + +const depsFingerprint = () => computeDepsFingerprint(pluginRoot); import net from "net"; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -96,29 +98,6 @@ function readPluginDeps() { } } -// Fingerprint of everything that decides which versions end up in node_modules. -// `overrides` matters as much as `dependencies` here: security pins for transitive -// packages live there, and a plugin upgrade that only moves a pin would otherwise -// leave an existing install on the old, vulnerable version. -function depsFingerprint() { - try { - const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - const hash = createHash("sha256").update( - JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), - ); - // npm resolves from the lockfile when one is present, so a lockfile-only change - // (a transitive bump that needed no override) also changes what lands on disk. - try { - hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); - } catch { - // No lockfile: package.json alone decides resolution. - } - return hash.digest("hex"); - } catch { - return null; - } -} - function readDepsStamp() { try { return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); @@ -136,24 +115,30 @@ const DEPS_LOCK_STALE_MS = 180_000; // a daemon mid-install would let the replacement import a half-written node_modules. let depsInstallInFlight = false; +// Contention and failure are different answers. Contention means another hook is +// installing, so the tree will be current shortly and this hook should leave the daemon +// alone. A genuine error means the refresh cannot happen at all, and pretending an +// install is in flight would keep a stale daemon serving the old tree indefinitely. function acquireDepsLock() { for (let attempt = 0; attempt < 2; attempt += 1) { try { - return openSync(DEPS_LOCK_PATH, "wx"); + return { fd: openSync(DEPS_LOCK_PATH, "wx"), contended: false }; } catch (err) { if (err.code !== "EEXIST") { process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); - return null; + return { fd: null, contended: false }; } try { - if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) return null; + if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) { + return { fd: null, contended: true }; + } unlinkSync(DEPS_LOCK_PATH); } catch { // The owner released it between our open and our stat. Try once more. } } } - return null; + return { fd: null, contended: true }; } function depsUpToDate(missing) { @@ -171,13 +156,20 @@ function ensureDepsInstalled() { // Serialise installs. After an upgrade every concurrent hook sees the same stale // stamp, and npm is not safe to run against one prefix from several processes. - const lockFd = acquireDepsLock(); - if (lockFd === null) { + const lock = acquireDepsLock(); + if (lock.fd === null) { + if (!lock.contended) { + // The lock could not be taken at all, so the refresh will not happen. Do not mark + // an install in flight: that would suppress daemon replacement and quietly keep the + // old tree scanning. Skip this event instead and let the next hook retry. + return false; + } // Another hook owns the install. The tree on disk is in flux, so this process must // not touch the daemon either; scan with what is already running, if anything. depsInstallInFlight = true; return !missing; } + const lockFd = lock.fd; try { // Recheck under the lock: whoever held it first may have finished the install. diff --git a/plugins/security/stackone-defender/scripts/defender-daemon.mjs b/plugins/security/stackone-defender/scripts/defender-daemon.mjs index f267871..b4a3787 100644 --- a/plugins/security/stackone-defender/scripts/defender-daemon.mjs +++ b/plugins/security/stackone-defender/scripts/defender-daemon.mjs @@ -12,7 +12,7 @@ import { dirname, join, resolve } from "path"; import { homedir } from "os"; import { fileURLToPath } from "url"; import { createServer } from "net"; -import { createHash } from "crypto"; +import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; import { unlinkSync, existsSync, readFileSync, appendFileSync, writeFileSync, mkdirSync, statSync, renameSync } from "fs"; const PROTOCOL_VERSION = 1; @@ -26,26 +26,6 @@ const DAEMON_STATE = join(homedir(), ".claude", "defender-daemon.json"); const scriptDir = dirname(fileURLToPath(import.meta.url)); const pluginRoot = resolve(scriptDir, ".."); const configPath = join(scriptDir, "defender-daemon.config.json"); -// Must stay in step with the same function in scan-tool-result.mjs. -function depsFingerprint() { - try { - const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - const hash = createHash("sha256").update( - JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), - ); - // npm resolves from the lockfile when one is present, so a lockfile-only change - // (a transitive bump that needed no override) also changes what lands on disk. - try { - hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); - } catch { - // No lockfile: package.json alone decides resolution. - } - return hash.digest("hex"); - } catch { - return null; - } -} - const requireFrom = createRequire(join(pluginRoot, "package.json")); function rotateLogIfNeeded() { @@ -296,9 +276,9 @@ server.listen(SOCKET_PATH, () => { const state = { pid: process.pid, defenderVersion, - // Same stamp the client writes after an install. Recording it lets the client - // tell that this daemon predates a dependency change and needs replacing. - depsStamp: depsFingerprint(), + // Computed here, not read from the client's stamp file, so the client can tell + // that this daemon predates a dependency change and needs replacing. + depsStamp: computeDepsFingerprint(pluginRoot), protocolVersion: PROTOCOL_VERSION, startedAt: new Date().toISOString(), socket: SOCKET_PATH, diff --git a/plugins/security/stackone-defender/scripts/deps-fingerprint.mjs b/plugins/security/stackone-defender/scripts/deps-fingerprint.mjs new file mode 100644 index 0000000..0062f65 --- /dev/null +++ b/plugins/security/stackone-defender/scripts/deps-fingerprint.mjs @@ -0,0 +1,30 @@ +/** + * Fingerprint of everything that decides which package versions end up in node_modules. + * + * Imported by both scan-tool-result.mjs and defender-daemon.mjs so the client's expected + * value and the daemon's recorded value can never drift. They are compared to decide + * whether a running daemon still matches the tree on disk, so two copies of this logic + * would mean either a kill/respawn loop on every scan or a permanently stale daemon. + */ +import { readFileSync } from "fs"; +import { join } from "path"; +import { createHash } from "crypto"; + +export function depsFingerprint(pluginRoot) { + try { + const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); + const hash = createHash("sha256").update( + JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), + ); + // npm resolves from the lockfile when one is present, so a lockfile-only change + // (a transitive bump that needed no override) also changes what lands on disk. + try { + hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); + } catch { + // No lockfile: package.json alone decides resolution. + } + return hash.digest("hex"); + } catch { + return null; + } +} diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index ef89bba..ba8f9e2 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -23,7 +23,9 @@ import { writeFileSync, } from "fs"; import { execSync, spawn } from "child_process"; -import { createHash } from "crypto"; +import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; + +const depsFingerprint = () => computeDepsFingerprint(pluginRoot); import net from "net"; const scriptDir = dirname(fileURLToPath(import.meta.url)); @@ -83,29 +85,6 @@ function readPluginDeps() { } } -// Fingerprint of everything that decides which versions end up in node_modules. -// `overrides` matters as much as `dependencies` here: security pins for transitive -// packages live there, and a plugin upgrade that only moves a pin would otherwise -// leave an existing install on the old, vulnerable version. -function depsFingerprint() { - try { - const pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf8")); - const hash = createHash("sha256").update( - JSON.stringify({ dependencies: pkg.dependencies ?? {}, overrides: pkg.overrides ?? {} }), - ); - // npm resolves from the lockfile when one is present, so a lockfile-only change - // (a transitive bump that needed no override) also changes what lands on disk. - try { - hash.update(readFileSync(join(pluginRoot, "package-lock.json"))); - } catch { - // No lockfile: package.json alone decides resolution. - } - return hash.digest("hex"); - } catch { - return null; - } -} - function readDepsStamp() { try { return readFileSync(DEPS_STAMP_PATH, "utf8").trim(); @@ -123,24 +102,30 @@ const DEPS_LOCK_STALE_MS = 180_000; // a daemon mid-install would let the replacement import a half-written node_modules. let depsInstallInFlight = false; +// Contention and failure are different answers. Contention means another hook is +// installing, so the tree will be current shortly and this hook should leave the daemon +// alone. A genuine error means the refresh cannot happen at all, and pretending an +// install is in flight would keep a stale daemon serving the old tree indefinitely. function acquireDepsLock() { for (let attempt = 0; attempt < 2; attempt += 1) { try { - return openSync(DEPS_LOCK_PATH, "wx"); + return { fd: openSync(DEPS_LOCK_PATH, "wx"), contended: false }; } catch (err) { if (err.code !== "EEXIST") { process.stderr.write(`[Defender] Dependency lock failed — scanner disabled: ${err.message}\n`); - return null; + return { fd: null, contended: false }; } try { - if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) return null; + if (Date.now() - statSync(DEPS_LOCK_PATH).mtimeMs <= DEPS_LOCK_STALE_MS) { + return { fd: null, contended: true }; + } unlinkSync(DEPS_LOCK_PATH); } catch { // The owner released it between our open and our stat. Try once more. } } } - return null; + return { fd: null, contended: true }; } function depsUpToDate(missing) { @@ -158,13 +143,20 @@ function ensureDepsInstalled() { // Serialise installs. After an upgrade every concurrent hook sees the same stale // stamp, and npm is not safe to run against one prefix from several processes. - const lockFd = acquireDepsLock(); - if (lockFd === null) { + const lock = acquireDepsLock(); + if (lock.fd === null) { + if (!lock.contended) { + // The lock could not be taken at all, so the refresh will not happen. Do not mark + // an install in flight: that would suppress daemon replacement and quietly keep the + // old tree scanning. Skip this event instead and let the next hook retry. + return false; + } // Another hook owns the install. The tree on disk is in flux, so this process must // not touch the daemon either; scan with what is already running, if anything. depsInstallInFlight = true; return !missing; } + const lockFd = lock.fd; try { // Recheck under the lock: whoever held it first may have finished the install. From f31c2104e19bebde2a5e2a11c4ae9aa6c8b34923 Mon Sep 17 00:00:00 2001 From: Guillaume Lebedel Date: Sat, 19 Sep 2026 01:18:50 +0100 Subject: [PATCH 8/8] fix(defender): run npm install without a shell MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The dependency install built a shell command string with the plugin's own installation directory interpolated into it: execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`) The hand-rolled quoting holds for spaces but not for a double quote. A prefix containing one closes the quoted argument and the rest of the path is parsed as shell, so an install path of the form /tmp/x"; ; echo " runs on every qualifying tool result, with the user's privileges. Demonstrated locally against the exact call shape before changing it. pluginRoot is derived from import.meta.url, so it is not attacker-supplied at runtime, but it is a path this code does not construct: plugins are cached under a marketplace-named directory, and a security plugin should not depend on the host sanitising that segment for its own safety. Whether any host actually permits such a segment is unverified, which is the argument for closing the sink rather than reasoning about its reachability. execFileSync passes the prefix as one argv element with no shell, so quotes, $(...) and semicolons arrive as literal characters. Verified: injection payloads now land as inert arguments, paths containing spaces still work, and npm still resolves without a shell under a volta-managed node and a PATH-only environment. Not a Windows regression: npm resolves as npm.cmd there and execFile would not find it, but the plugin is already POSIX-only — the daemon speaks over a filesystem unix socket at ~/.claude/defender.sock with no win32 handling. Unrelated to the six Aikido findings on this PR, which are false positives on readFileSync calls; this is the sink that sweep turned up and those rules missed. Both suites pass, 15 and 12. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01RNNCWE7ZTL1hkT7ciiHEHS --- .../scripts/scan-tool-result.mjs | 6 ++++-- .../security/stackone-defender/scripts/scan-tool-result.mjs | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs index 52c674a..51edca6 100755 --- a/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender-antigravity/scripts/scan-tool-result.mjs @@ -41,7 +41,7 @@ import { statSync, writeFileSync, } from "fs"; -import { execSync, spawn } from "child_process"; +import { execFileSync, spawn } from "child_process"; import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; const depsFingerprint = () => computeDepsFingerprint(pluginRoot); @@ -174,7 +174,9 @@ function ensureDepsInstalled() { try { // Recheck under the lock: whoever held it first may have finished the install. if (depsUpToDate(deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))))) return true; - execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { + // execFileSync, not a shell string: the prefix is an installation path we do not + // construct, and a quote in it would otherwise escape into an arbitrary command. + execFileSync("npm", ["install", "--prefix", pluginRoot, "--silent", "--no-audit", "--no-fund"], { timeout: 120_000, }); // Recompute after the install: npm normalises the lockfile, and the lockfile feeds diff --git a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs index ba8f9e2..5dd9cea 100755 --- a/plugins/security/stackone-defender/scripts/scan-tool-result.mjs +++ b/plugins/security/stackone-defender/scripts/scan-tool-result.mjs @@ -22,7 +22,7 @@ import { statSync, writeFileSync, } from "fs"; -import { execSync, spawn } from "child_process"; +import { execFileSync, spawn } from "child_process"; import { depsFingerprint as computeDepsFingerprint } from "./deps-fingerprint.mjs"; const depsFingerprint = () => computeDepsFingerprint(pluginRoot); @@ -161,7 +161,9 @@ function ensureDepsInstalled() { try { // Recheck under the lock: whoever held it first may have finished the install. if (depsUpToDate(deps.find((d) => !existsSync(join(pluginRoot, "node_modules", d))))) return true; - execSync(`npm install --prefix "${pluginRoot}" --silent --no-audit --no-fund`, { + // execFileSync, not a shell string: the prefix is an installation path we do not + // construct, and a quote in it would otherwise escape into an arbitrary command. + execFileSync("npm", ["install", "--prefix", pluginRoot, "--silent", "--no-audit", "--no-fund"], { timeout: 120_000, }); // Recompute after the install: npm normalises the lockfile, and the lockfile feeds