@@ -24,7 +24,6 @@ import readline from 'readline';
2424import fs from 'fs' ;
2525import path from 'path' ;
2626import os from 'os' ;
27- import crypto from 'crypto' ;
2827import { createShadowSnapshot , applyUndo , getSnapshotHistory , computeUndoDiff } from './undo' ;
2928import {
3029 getShield ,
@@ -1414,36 +1413,9 @@ program
14141413// ---------------------------------------------------------------------------
14151414// node9 shield — manage pre-packaged security rule templates
14161415// ---------------------------------------------------------------------------
1417-
1418- const SHIELD_CONFIG_PATH = path . join ( os . homedir ( ) , '.node9' , 'config.json' ) ;
1419-
1420- // Returns the parsed config, or null if the file doesn't exist, or exits on parse/IO error.
1421- // WARNING: no advisory lock — concurrent shield invocations use last-writer-wins semantics.
1422- function readRawConfig ( ) : Record < string , unknown > | null {
1423- try {
1424- const raw = fs . readFileSync ( SHIELD_CONFIG_PATH , 'utf-8' ) ;
1425- return JSON . parse ( raw ) as Record < string , unknown > ;
1426- } catch ( err : unknown ) {
1427- if ( ( err as NodeJS . ErrnoException ) . code === 'ENOENT' ) return null ; // file doesn't exist yet
1428- // Parse error or permission error — bail out to avoid overwriting valid config
1429- console . error (
1430- chalk . red (
1431- `\n❌ Cannot read ${ SHIELD_CONFIG_PATH } : ${ err instanceof Error ? err . message : String ( err ) } `
1432- )
1433- ) ;
1434- console . error ( chalk . red ( ' Aborting to avoid overwriting your existing config.\n' ) ) ;
1435- process . exit ( 1 ) ;
1436- }
1437- }
1438-
1439- function writeRawConfig ( config : Record < string , unknown > ) : void {
1440- // mkdirSync is idempotent with recursive:true — no TOCTOU concern here
1441- fs. mkdirSync ( path . dirname ( SHIELD_CONFIG_PATH ) , { recursive : true } ) ;
1442- // Random suffix avoids pid collision on concurrent CLI invocations
1443- const tmp = `${ SHIELD_CONFIG_PATH } .${ crypto . randomBytes ( 6 ) . toString ( 'hex' ) } .tmp` ;
1444- fs . writeFileSync ( tmp , JSON . stringify ( config , null , 2 ) , { mode : 0o600 } ) ;
1445- fs . renameSync ( tmp , SHIELD_CONFIG_PATH ) ;
1446- }
1416+ // Shields are applied dynamically at getConfig() load time by reading
1417+ // ~/.node9/shields.json and merging the catalog rules into the runtime policy.
1418+ // enable/disable only update shields.json — config.json is never touched.
14471419
14481420const shieldCmd = program
14491421 . command ( 'shield' )
@@ -1458,41 +1430,20 @@ shieldCmd
14581430 console . error ( chalk . red ( `\n❌ Unknown shield: "${ service } "\n` ) ) ;
14591431 console . log ( `Run ${ chalk . cyan ( 'node9 shield list' ) } to see available shields.\n` ) ;
14601432 process . exit ( 1 ) ;
1461- return ;
14621433 }
1463- const shield = getShield ( name ) ;
1464- if ( ! shield ) throw new Error ( `Shield "${ name } " resolved but not found — this is a bug` ) ;
1465-
1466- const config = readRawConfig ( ) ?? { } ;
1467- if ( ! config . policy || typeof config . policy !== 'object' ) config . policy = { } ;
1468- const policy = config . policy as Record < string , unknown > ;
1469-
1470- // Merge smartRules — deduplicate by name prefix
1471- const prefix = `shield:${ name } :` ;
1472- const existing = Array . isArray ( policy . smartRules )
1473- ? ( policy . smartRules as Array < { name ?: string } > )
1474- : [ ] ;
1475- policy . smartRules = [
1476- ...existing . filter ( ( r ) => ! r . name ?. startsWith ( prefix ) ) ,
1477- ...shield . smartRules ,
1478- ] ;
1479-
1480- // Merge dangerousWords — deduplicated
1481- const existingWords = Array . isArray ( policy . dangerousWords )
1482- ? ( policy . dangerousWords as string [ ] )
1483- : [ ] ;
1484- policy . dangerousWords = [ ...new Set ( [ ...existingWords , ...shield . dangerousWords ] ) ] ;
1485-
1486- config . policy = policy ;
1487- writeRawConfig ( config ) ;
1434+ const shield = getShield ( name ! ) ! ;
14881435
14891436 const active = readActiveShields ( ) ;
1490- if ( ! active . includes ( name ) ) writeActiveShields ( [ ...active , name ] ) ;
1437+ if ( active . includes ( name ! ) ) {
1438+ console . log ( chalk . yellow ( `\nℹ️ Shield "${ name } " is already active.\n` ) ) ;
1439+ return ;
1440+ }
1441+ writeActiveShields ( [ ...active , name ! ] ) ;
14911442
14921443 console . log ( chalk . green ( `\n🛡️ Shield "${ name } " enabled.` ) ) ;
1493- console . log ( chalk . gray ( ` Added ${ shield . smartRules . length } smart rules.` ) ) ;
1444+ console . log ( chalk . gray ( ` ${ shield . smartRules . length } smart rules now active .` ) ) ;
14941445 if ( shield . dangerousWords . length > 0 )
1495- console . log ( chalk . gray ( ` Added ${ shield . dangerousWords . length } dangerous words.` ) ) ;
1446+ console . log ( chalk . gray ( ` ${ shield . dangerousWords . length } dangerous words now active .` ) ) ;
14961447 if ( name === 'filesystem' ) {
14971448 console . log (
14981449 chalk . yellow (
@@ -1506,53 +1457,22 @@ shieldCmd
15061457
15071458shieldCmd
15081459 . command ( 'disable <service>' )
1509- . description ( 'Disable a security shield and remove its rules ' )
1460+ . description ( 'Disable a security shield' )
15101461 . action ( ( service : string ) => {
15111462 const name = resolveShieldName ( service ) ;
15121463 if ( ! name ) {
15131464 console . error ( chalk . red ( `\n❌ Unknown shield: "${ service } "\n` ) ) ;
15141465 console . log ( `Run ${ chalk . cyan ( 'node9 shield list' ) } to see available shields.\n` ) ;
15151466 process . exit ( 1 ) ;
1516- return ;
15171467 }
1518- const shield = getShield ( name ) ;
1519- if ( ! shield ) throw new Error ( `Shield "${ name } " resolved but not found — this is a bug` ) ;
15201468
15211469 const active = readActiveShields ( ) ;
1522- if ( ! active . includes ( name ) ) {
1470+ if ( ! active . includes ( name ! ) ) {
15231471 console . log ( chalk . yellow ( `\nℹ️ Shield "${ name } " is not active.\n` ) ) ;
15241472 return ;
15251473 }
15261474
1527- const config = readRawConfig ( ) ?? { } ;
1528- const remaining = active . filter ( ( s ) => s !== name ) ;
1529-
1530- // Only mutate policy if it already exists — avoid creating an empty policy object
1531- if ( config . policy && typeof config . policy === 'object' ) {
1532- const policy = config . policy as Record < string , unknown > ;
1533-
1534- // Remove this shield's smartRules
1535- const prefix = `shield:${ name } :` ;
1536- const rules = Array . isArray ( policy . smartRules )
1537- ? ( policy . smartRules as Array < { name ?: string } > )
1538- : [ ] ;
1539- policy . smartRules = rules . filter ( ( r ) => ! r . name ?. startsWith ( prefix ) ) ;
1540-
1541- // Remove dangerousWords, protecting words still needed by other active shields
1542- const protectedWords = new Set ( remaining . flatMap ( ( s ) => getShield ( s ) ?. dangerousWords ?? [ ] ) ) ;
1543- const shieldWords = new Set ( shield . dangerousWords ) ;
1544- const existingWords = Array . isArray ( policy . dangerousWords )
1545- ? ( policy . dangerousWords as string [ ] )
1546- : [ ] ;
1547- policy . dangerousWords = existingWords . filter (
1548- ( w ) => ! shieldWords . has ( w ) || protectedWords . has ( w )
1549- ) ;
1550-
1551- config . policy = policy ;
1552- writeRawConfig ( config ) ;
1553- }
1554-
1555- writeActiveShields ( remaining ) ;
1475+ writeActiveShields ( active . filter ( ( s ) => s !== name ) ) ;
15561476
15571477 console . log ( chalk . green ( `\n🛡️ Shield "${ name } " disabled.\n` ) ) ;
15581478 } ) ;
0 commit comments