140 lines
5.6 KiB
JavaScript
140 lines
5.6 KiB
JavaScript
|
|
import test from 'node:test';
|
||
|
|
import assert from 'node:assert/strict';
|
||
|
|
import fs from 'node:fs';
|
||
|
|
import os from 'node:os';
|
||
|
|
import path from 'node:path';
|
||
|
|
import { fileURLToPath } from 'node:url';
|
||
|
|
|
||
|
|
import {
|
||
|
|
parseScanOutput,
|
||
|
|
classifyVerdict,
|
||
|
|
resolveInstallerName,
|
||
|
|
discoverArtifacts,
|
||
|
|
} from '../../scripts/defender-scan.mjs';
|
||
|
|
import {
|
||
|
|
sha256File,
|
||
|
|
parseChecksumsFile,
|
||
|
|
checksumsContent,
|
||
|
|
expectedInstallDir,
|
||
|
|
dryRunInstallPath,
|
||
|
|
checkInstaller,
|
||
|
|
} from '../../scripts/offline-install-check.mjs';
|
||
|
|
|
||
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||
|
|
|
||
|
|
test('defender: clean output is parsed as clean', () => {
|
||
|
|
const parsed = parseScanOutput('Scan starting...\nScan finished.\nScanning X found no threats.\n');
|
||
|
|
assert.equal(parsed.status, 'clean');
|
||
|
|
assert.equal(parsed.threatCount, 0);
|
||
|
|
assert.equal(parsed.hasThreats, false);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: threat block exposes count and threat name', () => {
|
||
|
|
const text = 'Scanning X found 1 threats.\n\n<===========================LIST OF DETECTED THREATS==========================>\nThreat : Virus:DOS/EICAR_Test_File\nResources : 1 total\n file : X\n';
|
||
|
|
const parsed = parseScanOutput(text);
|
||
|
|
assert.equal(parsed.status, 'threat');
|
||
|
|
assert.equal(parsed.threatCount, 1);
|
||
|
|
assert.ok(parsed.threatNames.includes('Virus:DOS/EICAR_Test_File'));
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: remediating-mode threat output is recognized', () => {
|
||
|
|
const parsed = parseScanOutput('Scanning X found 2 threats.\nCleaning started...\n');
|
||
|
|
assert.equal(parsed.status, 'threat');
|
||
|
|
assert.equal(parsed.threatCount, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: skipped output is not treated as clean', () => {
|
||
|
|
const parsed = parseScanOutput('Scan starting...\nScan finished.\nScanning D:\\abc was skipped.\n');
|
||
|
|
assert.equal(parsed.status, 'skipped');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: verdict classification', () => {
|
||
|
|
assert.equal(classifyVerdict([{ status: 'clean' }]), 'PASS');
|
||
|
|
assert.equal(classifyVerdict([{ status: 'clean' }, { status: 'threat' }]), 'THREAT');
|
||
|
|
assert.equal(classifyVerdict([{ status: 'error' }]), 'FAIL');
|
||
|
|
assert.equal(classifyVerdict([{ status: 'timeout' }]), 'FAIL');
|
||
|
|
assert.equal(classifyVerdict([{ status: 'skipped' }]), 'PASS');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: installer name resolution from electron-builder template', () => {
|
||
|
|
const config = { artifactName: 'aps-agent-desktop-' + '$' + '{version}' + '.' + '$' + '{ext}', version: '0.1.0' };
|
||
|
|
assert.equal(resolveInstallerName(config), 'aps-agent-desktop-0.1.0.exe');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('defender: artifact discovery is driven by apps/desktop/package.json', (t) => {
|
||
|
|
const artifacts = discoverArtifacts(repoRoot);
|
||
|
|
assert.deepEqual(
|
||
|
|
artifacts.map((a) => a.id),
|
||
|
|
['sidecar-exe', 'desktop-frozen-exe', 'bundled-sidecar-exe', 'nsis-installer'],
|
||
|
|
);
|
||
|
|
const installer = artifacts.find((a) => a.id === 'nsis-installer');
|
||
|
|
assert.ok(installer.file.endsWith('aps-agent-desktop-0.1.0.exe'));
|
||
|
|
if (!installer.exists) t.skip('NSIS installer not built on this machine');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('offline: sha256File is deterministic lowercase hex', async () => {
|
||
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-node-test-'));
|
||
|
|
try {
|
||
|
|
const file = path.join(dir, 'sample.bin');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(64 * 1024, 3));
|
||
|
|
const first = await sha256File(file);
|
||
|
|
const second = await sha256File(file);
|
||
|
|
assert.equal(first, second);
|
||
|
|
assert.match(first, /^[0-9a-f]{64}$/);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('offline: checksumsContent and parseChecksumsFile round trip', async () => {
|
||
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-node-test-'));
|
||
|
|
try {
|
||
|
|
const file = path.join(dir, 'installer.exe');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(1024, 1));
|
||
|
|
const hash = await sha256File(file);
|
||
|
|
const content = checksumsContent([{ file, sha256: hash }], dir);
|
||
|
|
const parsed = parseChecksumsFile(content);
|
||
|
|
assert.equal(parsed.length, 1);
|
||
|
|
assert.equal(parsed[0].sha256, hash);
|
||
|
|
assert.equal(parsed[0].file, 'installer.exe');
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('offline: expected install dir is LOCALAPPDATA Programs productName', () => {
|
||
|
|
const dir = expectedInstallDir('C:\\Users\\tester\\AppData\\Local', '工业智核 APS');
|
||
|
|
assert.equal(dir, 'C:\\Users\\tester\\AppData\\Local\\Programs\\工业智核 APS');
|
||
|
|
});
|
||
|
|
|
||
|
|
test('offline: dry-run detects absent and complete install layouts', () => {
|
||
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-node-test-'));
|
||
|
|
try {
|
||
|
|
const absent = dryRunInstallPath(path.join(dir, 'missing-local'), '工业智核 APS');
|
||
|
|
assert.equal(absent.exists, false);
|
||
|
|
const local = path.join(dir, 'LocalAppData');
|
||
|
|
const installDir = path.join(local, 'Programs', '工业智核 APS');
|
||
|
|
fs.mkdirSync(path.join(installDir, 'resources', 'sidecar'), { recursive: true });
|
||
|
|
fs.writeFileSync(path.join(installDir, '工业智核 APS.exe'), 'app');
|
||
|
|
fs.writeFileSync(path.join(installDir, 'resources', 'sidecar', 'aps-sidecar.exe'), 'sidecar');
|
||
|
|
const complete = dryRunInstallPath(local, '工业智核 APS');
|
||
|
|
assert.equal(complete.exists, true);
|
||
|
|
assert.equal(complete.layoutValid, true);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
test('offline: installer size gate', () => {
|
||
|
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'aps-node-test-'));
|
||
|
|
try {
|
||
|
|
const file = path.join(dir, 'big.exe');
|
||
|
|
fs.writeFileSync(file, Buffer.alloc(4096, 0));
|
||
|
|
assert.equal(checkInstaller(file, 1024).sizeOk, true);
|
||
|
|
assert.equal(checkInstaller(path.join(dir, 'nope.exe'), 1024).exists, false);
|
||
|
|
assert.equal(checkInstaller(file, 10 * 1024).sizeOk, false);
|
||
|
|
} finally {
|
||
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
||
|
|
}
|
||
|
|
});
|