1- import { execFileSync } from "node:child_process" ;
1+ import { execFile } from "node:child_process" ;
22import { copyFileSync , mkdirSync , mkdtempSync , readdirSync , readFileSync , rmSync } from "node:fs" ;
33import { tmpdir } from "node:os" ;
44import { dirname , join , relative , resolve , sep } from "node:path" ;
5+ import { promisify } from "node:util" ;
56import { afterEach , describe , expect , it } from "vitest" ;
67import { isScannable , scanDirectoryWithSummary } from "../security/skill-scanner.js" ;
78
@@ -18,6 +19,9 @@ type PublishablePluginPackage = {
1819 packageName : string ;
1920} ;
2021
22+ const execFileAsync = promisify ( execFile ) ;
23+ const PACKAGE_SCAN_CONCURRENCY = 6 ;
24+
2125const REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS = new Set ( [
2226 "@openclaw/acpx:dangerous-exec:src/codex-auth-bridge.ts" ,
2327 "@openclaw/acpx:dangerous-exec:src/runtime-internals/mcp-proxy.mjs" ,
@@ -61,14 +65,17 @@ function parseNpmPackFiles(raw: string, packageName: string): string[] {
6165 . toSorted ( ) ;
6266}
6367
64- function collectNpmPackedFiles ( packageDir : string , packageName : string ) : string [ ] {
65- const raw = execFileSync ( "npm" , [ "pack" , "--dry-run" , "--json" , "--ignore-scripts" ] , {
66- cwd : packageDir ,
67- encoding : "utf8" ,
68- maxBuffer : 128 * 1024 * 1024 ,
69- stdio : [ "ignore" , "pipe" , "pipe" ] ,
70- } ) ;
71- return parseNpmPackFiles ( raw , packageName ) ;
68+ async function collectNpmPackedFiles ( packageDir : string , packageName : string ) : Promise < string [ ] > {
69+ const { stdout } = await execFileAsync (
70+ "npm" ,
71+ [ "pack" , "--dry-run" , "--json" , "--ignore-scripts" ] ,
72+ {
73+ cwd : packageDir ,
74+ encoding : "utf8" ,
75+ maxBuffer : 128 * 1024 * 1024 ,
76+ } ,
77+ ) ;
78+ return parseNpmPackFiles ( stdout , packageName ) ;
7279}
7380
7481function isScannerWalkedPackedPath ( packedPath : string ) : boolean {
@@ -141,6 +148,72 @@ function collectPublishablePluginPackages(): PublishablePluginPackage[] {
141148 . toSorted ( ( left , right ) => left . packageName . localeCompare ( right . packageName ) ) ;
142149}
143150
151+ async function mapWithConcurrency < T , U > (
152+ items : readonly T [ ] ,
153+ concurrency : number ,
154+ fn : ( item : T ) => Promise < U > ,
155+ ) : Promise < U [ ] > {
156+ const results = new Array < U > ( items . length ) ;
157+ let nextIndex = 0 ;
158+ const workerCount = Math . min ( concurrency , items . length ) ;
159+ await Promise . all (
160+ Array . from ( { length : workerCount } , async ( ) => {
161+ while ( nextIndex < items . length ) {
162+ const index = nextIndex ;
163+ nextIndex += 1 ;
164+ results [ index ] = await fn ( items [ index ] ! ) ;
165+ }
166+ } ) ,
167+ ) ;
168+ return results ;
169+ }
170+
171+ async function scanPublishablePluginPackage ( plugin : PublishablePluginPackage ) : Promise < {
172+ reviewedCriticalFindings : string [ ] ;
173+ expectedReviewedCriticalFindings : string [ ] ;
174+ unexpectedCriticalFindings : string [ ] ;
175+ } > {
176+ const reviewedCriticalFindings : string [ ] = [ ] ;
177+ const expectedReviewedCriticalFindings : string [ ] = [ ] ;
178+ const unexpectedCriticalFindings : string [ ] = [ ] ;
179+ const packedFiles = await collectNpmPackedFiles ( plugin . packageDir , plugin . packageName ) ;
180+ for ( const packedFile of packedFiles ) {
181+ const key = `${ plugin . packageName } :dangerous-exec:${ normalizePackedFindingPath ( packedFile ) } ` ;
182+ if ( OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS . has ( key ) ) {
183+ expectedReviewedCriticalFindings . push ( key ) ;
184+ }
185+ }
186+ const stageDir = stageScannerRelevantPackedFiles ( plugin . packageDir , packedFiles ) ;
187+ const summary = await scanDirectoryWithSummary ( stageDir , {
188+ excludeTestFiles : true ,
189+ maxFiles : 10_000 ,
190+ } ) ;
191+
192+ for ( const finding of summary . findings ) {
193+ if ( finding . severity !== "critical" ) {
194+ continue ;
195+ }
196+ const packedPath = normalizePackedFindingPath (
197+ relative ( stageDir , finding . file ) . split ( sep ) . join ( "/" ) ,
198+ ) ;
199+ const key = `${ plugin . packageName } :${ finding . ruleId } :${ packedPath } ` ;
200+ if (
201+ REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS . has ( key ) ||
202+ OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS . has ( key )
203+ ) {
204+ reviewedCriticalFindings . push ( key ) ;
205+ continue ;
206+ }
207+ unexpectedCriticalFindings . push ( [ key , `${ finding . line } ` , finding . evidence ] . join ( ":" ) ) ;
208+ }
209+
210+ return {
211+ reviewedCriticalFindings,
212+ expectedReviewedCriticalFindings,
213+ unexpectedCriticalFindings,
214+ } ;
215+ }
216+
144217describe ( "publishable plugin npm package install security scan" , ( ) => {
145218 it ( "keeps npm-published plugin files clear of unexpected critical hits" , async ( ) => {
146219 const unexpectedCriticalFindings : string [ ] = [ ] ;
@@ -149,37 +222,22 @@ describe("publishable plugin npm package install security scan", () => {
149222 REQUIRED_REVIEWED_PUBLISHABLE_CRITICAL_FINDINGS ,
150223 ) ;
151224
152- for ( const plugin of collectPublishablePluginPackages ( ) ) {
153- const packedFiles = collectNpmPackedFiles ( plugin . packageDir , plugin . packageName ) ;
154- for ( const packedFile of packedFiles ) {
155- const key = `${ plugin . packageName } :dangerous-exec:${ normalizePackedFindingPath ( packedFile ) } ` ;
156- if ( OPTIONAL_REVIEWED_PUBLISHABLE_DIST_CRITICAL_FINDINGS . has ( key ) ) {
157- expectedReviewedCriticalFindings . add ( key ) ;
158- }
225+ const packageResults = await mapWithConcurrency (
226+ collectPublishablePluginPackages ( ) ,
227+ PACKAGE_SCAN_CONCURRENCY ,
228+ scanPublishablePluginPackage ,
229+ ) ;
230+ for ( const result of packageResults ) {
231+ for ( const key of result . expectedReviewedCriticalFindings ) {
232+ expectedReviewedCriticalFindings . add ( key ) ;
159233 }
160- const stageDir = stageScannerRelevantPackedFiles ( plugin . packageDir , packedFiles ) ;
161- const summary = await scanDirectoryWithSummary ( stageDir , {
162- excludeTestFiles : true ,
163- maxFiles : 10_000 ,
164- } ) ;
165-
166- for ( const finding of summary . findings ) {
167- if ( finding . severity !== "critical" ) {
168- continue ;
169- }
170- const packedPath = normalizePackedFindingPath (
171- relative ( stageDir , finding . file ) . split ( sep ) . join ( "/" ) ,
172- ) ;
173- const key = `${ plugin . packageName } :${ finding . ruleId } :${ packedPath } ` ;
174- if ( expectedReviewedCriticalFindings . has ( key ) ) {
175- reviewedCriticalFindings . add ( key ) ;
176- continue ;
177- }
178- unexpectedCriticalFindings . push ( [ key , `${ finding . line } ` , finding . evidence ] . join ( ":" ) ) ;
234+ for ( const key of result . reviewedCriticalFindings ) {
235+ reviewedCriticalFindings . add ( key ) ;
179236 }
237+ unexpectedCriticalFindings . push ( ...result . unexpectedCriticalFindings ) ;
180238 }
181239
182- expect ( unexpectedCriticalFindings ) . toEqual ( [ ] ) ;
240+ expect ( unexpectedCriticalFindings . toSorted ( ) ) . toEqual ( [ ] ) ;
183241 expect ( [ ...reviewedCriticalFindings ] . toSorted ( ) ) . toEqual (
184242 [ ...expectedReviewedCriticalFindings ] . toSorted ( ) ,
185243 ) ;
0 commit comments