Add packaged DLP broker and protected Windows service foundation
This commit is contained in:
1 parent
95d23f4e0b
commit
7b5057901a
140 files changed
+11215
-60
No files matched your search
+60
-17
@@ -1,26 +1,69 @@
|
||||
import {hardenedElectronFuses,verifyPackagedFuses} from './package-fuses.mjs';
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import {readFileSync,readdirSync} from 'node:fs';
|
||||
import {createHash} from 'node:crypto';
|
||||
import {spawnSync} from 'node:child_process';
|
||||
import {readFileSync,readdirSync,writeFileSync,createReadStream} from 'node:fs';
|
||||
import {createHash,randomUUID} from 'node:crypto';
|
||||
import {createRequire} from 'node:module';
|
||||
import path from 'node:path';
|
||||
const platform=process.argv[2];if(platform!=='win'||process.platform!=='win32')throw Error('Windows is the active build target; other platforms are deferred');
|
||||
const runtime=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-windows-runtime.py'],{stdio:'inherit'});if(runtime.status!==0)process.exit(runtime.status??1);
|
||||
const semantic=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',['scripts/provision-semantic-runtime.py','--verify'],{stdio:'inherit'});if(semantic.status!==0)process.exit(semantic.status??1);
|
||||
import {provisionBrokerRuntime,BROKER_RUNTIME} from './provision-broker-runtime.mjs';
|
||||
import {prepareBrokerResources,verifyPackagedBroker,embeddedBrokerManifestReader,snapshotScannerResources,verifyScannerResources} from './package-broker-resources.mjs';
|
||||
import {createWindowsPackagePlan} from './windows-package-plan.mjs';
|
||||
import {buildVerifiedInstaller} from './verified-installer.mjs';
|
||||
const platform=process.argv[2];
|
||||
if(platform!=='win'||process.platform!=='win32'||process.argv.slice(3).some(value=>value!=='--unpacked-only'))throw Error('Windows is the active build target; only --unpacked-only is additionally supported');
|
||||
const workspace=path.resolve('.');
|
||||
for(const args of [['scripts/provision-windows-runtime.py'],['scripts/provision-semantic-runtime.py','--verify']]){
|
||||
const result=spawnSync(process.env.SAFETYNET_BUILD_PYTHON||'python',args,{stdio:'inherit'});if(result.status!==0)process.exit(result.status??1);
|
||||
}
|
||||
await provisionBrokerRuntime(workspace);
|
||||
const name=process.env.BRAND_NAME||'SafetyNet',id=process.env.BRAND_APP_ID||'enterprise.safetynet.browser';
|
||||
if(!/^[\w .-]{1,40}$/.test(name)||!/^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9]*){2,}$/.test(id))throw Error('Invalid white-label product name or app ID');
|
||||
const configuration=JSON.parse(readFileSync('package.json','utf8')),workspace=path.resolve('.'),output=path.resolve(configuration.build.directories.output);
|
||||
if(!output.startsWith(workspace+path.sep))throw Error('Build output must stay inside the workspace');
|
||||
const configuration=JSON.parse(readFileSync('package.json','utf8')),output=path.resolve(configuration.build.directories.output);
|
||||
// Reuse the pinned installed distribution already exercised by native tests.
|
||||
// This avoids a second archive extraction and Windows directory-rename locks.
|
||||
const electronDirectory=path.dirname(createRequire(import.meta.url)('electron'));
|
||||
if(readFileSync(path.join(electronDirectory,'version'),'utf8').trim().replace(/^v/,'')!==configuration.devDependencies.electron)throw Error('Installed Electron does not match the build pin');
|
||||
function sourceSnapshot(){const entries=[];function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}for(const directory of ['apps/browser','packages/core'])walk(directory);entries.push(['package.json',createHash('sha256').update(readFileSync('package.json')).digest('hex')]);return JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])));}
|
||||
const plan=createWindowsPackagePlan({workspace,outputDirectory:output,electronDirectory,productName:name,appId:id});
|
||||
const broker=await prepareBrokerResources({root:workspace,build:configuration.build});
|
||||
const scannerSnapshot=await snapshotScannerResources(workspace);
|
||||
const artifactName=`${name}-Setup-${configuration.version}.exe`;
|
||||
if(!/^[\w .-]+\.exe$/.test(artifactName))throw Error('Invalid installer artifact name');
|
||||
const buildConfiguration={...broker.configuration,artifactName,electronFuses:hardenedElectronFuses};
|
||||
const configPath=path.join(workspace,'.runtime/windows',`package-build-${randomUUID()}.json`);
|
||||
writeFileSync(configPath,JSON.stringify(buildConfiguration,null,2)+'\n',{flag:'wx'});
|
||||
const configArgs=[`--config=${configPath}`];
|
||||
function sourceSnapshot(){
|
||||
const entries=[];
|
||||
function walk(directory){for(const item of readdirSync(directory,{withFileTypes:true})){const file=path.join(directory,item.name);if(item.isSymbolicLink())throw Error('Build source reparse point rejected');if(item.isDirectory())walk(file);else if(item.isFile())entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);}}
|
||||
for(const directory of ['apps/browser','apps/network-broker','apps/inspector','packages/core','scripts'])walk(directory);
|
||||
for(const file of ['package.json','package-lock.json',configuration.build.win.icon,configPath])entries.push([file,createHash('sha256').update(readFileSync(file)).digest('hex')]);
|
||||
return createHash('sha256').update(JSON.stringify(entries.sort((a,b)=>a[0].localeCompare(b[0])))).digest('hex');
|
||||
}
|
||||
const sourcesBefore=sourceSnapshot();
|
||||
const r=spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',`--${platform}`,'--dir',`--config.electronDist=${electronDirectory}`,`--config.productName=${name}`,`--config.appId=${id}`,...Object.entries(hardenedElectronFuses).map(([key,value])=>`--config.electronFuses.${key}=${value}`)],{stdio:'inherit',env:process.env});
|
||||
if(r.status!==0)process.exit(r.status??1);
|
||||
if(sourceSnapshot()!==sourcesBefore)throw Error('Browser sources changed during packaging; discard this artifact and rebuild from stable sources');
|
||||
const resources=path.join(output,'win-unpacked/resources');
|
||||
await verifyPackagedFuses(path.join(output,'win-unpacked',`${name}.exe`),path.join(resources,'app.asar'));
|
||||
console.log('Packaged Electron security fuses and embedded ASAR integrity verified.');
|
||||
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});process.exit(acl.status??1);
|
||||
const run=args=>spawnSync(process.execPath,['node_modules/electron-builder/out/cli/cli.js',...args,...configArgs],{stdio:'inherit',env:process.env});
|
||||
const result=run(plan.unpacked.args);if(result.status!==0)process.exit(result.status??1);
|
||||
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
|
||||
const resources=path.join(plan.unpacked.directory,'resources'),archive=path.join(resources,'app.asar'),executable=path.join(plan.unpacked.directory,`${name}.exe`);
|
||||
async function verify(){
|
||||
await broker.verifySources();if(sourceSnapshot()!==sourcesBefore)throw Error('Build sources changed; discard this package');
|
||||
await verifyPackagedFuses(executable,archive);
|
||||
await verifyPackagedBroker({resources,expectedManifest:broker.manifest,readEmbeddedManifest:embeddedBrokerManifestReader(archive)});
|
||||
await verifyScannerResources(resources,scannerSnapshot);
|
||||
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py')],{stdio:'inherit',windowsHide:true});if(acl.status!==0)throw Error('Packaged scanner ACL verification failed');
|
||||
return {identity:await readIdentity(),fuses:true,resources:true,broker:true,runtimeAcl:true};
|
||||
}
|
||||
// This verifies/provisions the unpacked build location only. NSIS extraction
|
||||
// does not preserve these ACEs: a reviewed installer post-install ACL step and
|
||||
// actual standard-user Program Files acceptance remain required. Browser
|
||||
// startup must NEVER silently provision or broaden installation permissions.
|
||||
const acl=spawnSync(path.join(resources,'python/python.exe'),[path.join(resources,'inspector/provision_runtime_acl.py'),'--provision'],{stdio:'inherit',windowsHide:true});if(acl.status!==0)process.exit(acl.status??1);
|
||||
async function treeHash(directory){
|
||||
const hash=createHash('sha256');
|
||||
async function walk(current){for(const item of readdirSync(current,{withFileTypes:true}).sort((a,b)=>a.name.localeCompare(b.name))){const file=path.join(current,item.name);if(item.isSymbolicLink())throw Error('Packaged reparse point rejected');if(item.isDirectory())await walk(file);else if(item.isFile()){const content=createHash('sha256');let bytes=0;for await(const chunk of createReadStream(file)){content.update(chunk);bytes+=chunk.length;}hash.update(JSON.stringify([path.relative(directory,file).replaceAll('\\','/'),bytes,content.digest('hex')])+'\n');}}}
|
||||
await walk(directory);return hash.digest('hex');
|
||||
}
|
||||
async function readIdentity(){await broker.verifySources();const scanners=await snapshotScannerResources(workspace);if(JSON.stringify(scanners)!==JSON.stringify(scannerSnapshot))throw Error('Scanner sources changed; discard this package');return {sourceSha256:sourceSnapshot(),runtimeSha256:createHash('sha256').update(JSON.stringify({electron:configuration.devDependencies.electron,broker:BROKER_RUNTIME,scanners})).digest('hex'),packageSha256:await treeHash(plan.unpacked.directory)};}
|
||||
await verify();
|
||||
console.log('Unpacked browser, embedded broker manifest, complete broker payload, security fuses and scanner ACLs verified.');
|
||||
if(!process.argv.includes('--unpacked-only')){
|
||||
const installer=await buildVerifiedInstaller({plan,expectedIdentity:await readIdentity(),readIdentity,verify,run,installerPath:path.join(output,artifactName)});
|
||||
console.log(JSON.stringify({installer:installer.path,bytes:installer.bytes,sha256:installer.sha256,brokerIncluded:true,productionActivationVerified:false}));
|
||||
}
|
||||
Reference in new issue
Block a user