34 lines
3.3 KiB
JavaScript
34 lines
3.3 KiB
JavaScript
'use strict';
|
|||
|
|
const {execFile}=require('node:child_process');
|
||
|
|
const path=require('node:path');
|
||
|
|
const net=require('node:net');
|
||
|
|
// No administrator input is interpolated into PowerShell. Adapter matching happens
|
||
|
|
// after bounded JSON parsing; the deployment cannot turn names into shell code.
|
||
|
|
const postureScript="$ErrorActionPreference='Stop'; $dnsRecords=@(Get-DnsClientServerAddress -ErrorAction Stop); $adapters=@(Get-NetAdapter -IncludeHidden | Where-Object {$_.Status -eq 'Up'} | ForEach-Object { $adapter=$_; $dns=@($dnsRecords | Where-Object {$_.InterfaceIndex -eq $adapter.ifIndex} | ForEach-Object {$_.ServerAddresses}); [pscustomobject]@{name=$adapter.Name; dns=$dns} }); ConvertTo-Json -InputObject $adapters -Depth 4 -Compress";
|
||
|
|
function resolverOptions(dns={mode:'system',templates:[]}){
|
||
|
|
if(dns.mode==='system')return {enableBuiltInResolver:false,secureDnsMode:'off',secureDnsServers:[]};
|
||
|
|
if(dns.mode!=='secure'||!Array.isArray(dns.templates)||!dns.templates.length||dns.templates.length>8)throw Error('Managed DNS configuration is invalid');
|
||
|
|
for(const template of dns.templates){const u=new URL(template.replace('{?dns}',''));if(u.protocol!=='https:'||u.username||u.password||u.hash)throw Error('Managed DNS requires HTTPS templates');}
|
||
|
|
return {enableBuiltInResolver:true,secureDnsMode:'secure',secureDnsServers:[...dns.templates]};
|
||
|
|
}
|
||
|
|
function evaluatePosture(vpn,adapters){
|
||
|
|
if(!vpn?.required)return {allowed:true,reason:'not-required'};
|
||
|
|
if(!Array.isArray(vpn.interfaceNames)||!vpn.interfaceNames.length||!Array.isArray(vpn.requiredDnsServers)||vpn.requiredDnsServers.some(ip=>!net.isIP(ip)))return {allowed:false,reason:'configuration-invalid'};
|
||
|
|
if(!Array.isArray(adapters)||adapters.length>256)return {allowed:false,reason:'probe-invalid'};
|
||
|
|
const matches=adapters.filter(a=>typeof a?.name==='string'&&Array.isArray(a.dns)&&vpn.interfaceNames.some(name=>name.toLowerCase()===a.name.toLowerCase()));
|
||
|
|
return matches.some(a=>vpn.requiredDnsServers.every(ip=>a.dns.some(found=>typeof found==='string'&&found.toLowerCase()===ip.toLowerCase())))?{allowed:true,reason:'approved-interface-present'}:{allowed:false,reason:'required-interface-or-dns-missing'};
|
||
|
|
}
|
||
|
|
function probeWindows({platform=process.platform,run=execFile}={}){
|
||
|
|
if(platform!=='win32')return Promise.reject(Error('Network posture is available only on Windows'));
|
||
|
|
const executable=path.join(process.env.SystemRoot||'C:\\Windows','System32','WindowsPowerShell','v1.0','powershell.exe');
|
||
|
|
return new Promise((resolve,reject)=>run(executable,['-NoLogo','-NoProfile','-NonInteractive','-Command',postureScript],{windowsHide:true,timeout:10000,maxBuffer:65536,encoding:'utf8'},(error,stdout)=>{
|
||
|
|
if(error)return reject(Error('Network posture probe unavailable'));
|
||
|
|
try{const value=JSON.parse(stdout.replace(/^\uFEFF/,''));if(!Array.isArray(value))throw Error();resolve(value);}catch{reject(Error('Network posture response invalid'));}
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
function createNetworkPosture({probe=probeWindows}={}){
|
||
|
|
let pending;
|
||
|
|
return {async check(vpn){if(!vpn?.required)return {allowed:true,reason:'not-required'};if(!pending)pending=Promise.resolve().then(probe).finally(()=>{pending=null;});try{return evaluatePosture(vpn,await pending);}catch{return {allowed:false,reason:'probe-unavailable'};}}};
|
||
|
|
}
|
||
|
|
module.exports={resolverOptions,evaluatePosture,probeWindows,createNetworkPosture};
|
||
|
|
|