fix(marketplace): close client review findings
This commit is contained in:
@@ -318,20 +318,22 @@ export function verifyMarketplaceClientArtifact(appAsarContents, productionTrust
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Marketplace trust source from the packaged application itself.
|
||||
* Checkout sources are not evidence of what an installed app will trust.
|
||||
* Read the package.main-reachable application graph from the packaged app.
|
||||
* Checkout sources and unrelated app.asar files are not product evidence.
|
||||
*/
|
||||
export async function readPackagedMarketplaceTrustSource(appAsar) {
|
||||
async function readPackagedApplicationGraph(appAsar) {
|
||||
const entries = new Set(listPackage(appAsar, { isPack: false })
|
||||
.map((entry) => entry.replace(/^[/\\]+/u, '').replaceAll('\\', '/')));
|
||||
const readArchiveText = (entry) => Buffer.from(
|
||||
extractFile(appAsar, entry.replaceAll('/', sep)),
|
||||
).toString('utf8');
|
||||
const readArchiveText = (entry) => {
|
||||
if (!entries.has(entry)) throw new Error(`Packaged app.asar entry is missing: ${entry}`);
|
||||
return Buffer.from(extractFile(appAsar, entry.replaceAll('/', sep))).toString('utf8');
|
||||
};
|
||||
let packageJson;
|
||||
try {
|
||||
packageJson = JSON.parse(readArchiveText('package.json'));
|
||||
} catch {
|
||||
throw new Error('Packaged app.asar package.json is unreadable');
|
||||
} catch (error) {
|
||||
const reason = error instanceof Error ? error.message : 'unknown error';
|
||||
throw new Error(`Packaged app.asar package.json is unreadable: ${reason}`);
|
||||
}
|
||||
if (typeof packageJson.main !== 'string' || packageJson.main.trim().length === 0) {
|
||||
throw new Error('Packaged app.asar package.json.main is missing');
|
||||
@@ -348,9 +350,10 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
|
||||
}
|
||||
return result.join('/');
|
||||
};
|
||||
const resolveModule = (from, specifier) => {
|
||||
const resolveEntry = (from, specifier) => {
|
||||
if (!specifier.startsWith('.')) return null;
|
||||
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${specifier}`);
|
||||
const withoutSuffix = specifier.split(/[?#]/u, 1)[0];
|
||||
const base = normalizeAsarPath(`${dirname(from).replaceAll('\\', '/')}/${withoutSuffix}`);
|
||||
if (!base) return null;
|
||||
const candidates = [base, `${base}.js`, `${base}.mjs`, `${base}.cjs`, `${base}/index.js`];
|
||||
return candidates.find((candidate) => entries.has(candidate)) ?? null;
|
||||
@@ -372,16 +375,28 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
|
||||
}
|
||||
reachable.push({ entry, source });
|
||||
const specifiers = [];
|
||||
const importPattern = /(?:import|export)\s+(?:[\s\S]*?\sfrom\s*)?['"]([^'"]+)['"]/gu;
|
||||
const importPattern = /\b(?:import|export)(?:[^"'`;]*?\bfrom)?\s*['"]([^'"]+)['"]/gu;
|
||||
const dynamicImportPattern = /\bimport\(\s*['"]([^'"]+)['"]\s*\)/gu;
|
||||
const requirePattern = /\brequire\(\s*['"]([^'"]+)['"]\s*\)/gu;
|
||||
const htmlEntryPattern = /['"]([^'"]+\.html(?:[?#][^'"]*)?)['"]/gu;
|
||||
const htmlAssetPattern = /\b(?:src|href)\s*=\s*['"]([^'"]+)['"]/gu;
|
||||
for (const match of source.matchAll(importPattern)) specifiers.push(match[1]);
|
||||
for (const match of source.matchAll(dynamicImportPattern)) specifiers.push(match[1]);
|
||||
for (const match of source.matchAll(requirePattern)) specifiers.push(match[1]);
|
||||
for (const match of source.matchAll(htmlEntryPattern)) specifiers.push(match[1]);
|
||||
if (entry.endsWith('.html')) {
|
||||
for (const match of source.matchAll(htmlAssetPattern)) specifiers.push(match[1]);
|
||||
}
|
||||
for (const specifier of specifiers) {
|
||||
const resolved = resolveModule(entry, specifier);
|
||||
const resolved = resolveEntry(entry, specifier);
|
||||
if (resolved) pending.push(resolved);
|
||||
}
|
||||
}
|
||||
|
||||
return { packageJson, reachable };
|
||||
}
|
||||
|
||||
function findPackagedMarketplaceTrustSource(reachable) {
|
||||
for (const { source } of reachable) {
|
||||
const trustAssignment = source.match(
|
||||
/CODE_OWNED_PLUGIN_SIGNING_KEYS\s*=\s*Object\.freeze\(\s*\{\}\s*(?:as\s+[^)]*)?\)/u,
|
||||
@@ -398,6 +413,31 @@ export async function readPackagedMarketplaceTrustSource(appAsar) {
|
||||
throw new Error('Packaged app.asar does not contain the Marketplace trust source');
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the Marketplace trust source from the packaged application itself.
|
||||
* Checkout sources are not evidence of what an installed app will trust.
|
||||
*/
|
||||
export async function readPackagedMarketplaceTrustSource(appAsar) {
|
||||
const { reachable } = await readPackagedApplicationGraph(appAsar);
|
||||
return findPackagedMarketplaceTrustSource(reachable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify Marketplace contract markers only in the package.main-reachable app
|
||||
* graph. An arbitrary string elsewhere in app.asar is not evidence that the
|
||||
* installed Main/Renderer contract is present.
|
||||
*/
|
||||
export async function verifyPackagedMarketplaceClientArtifact(appAsar) {
|
||||
const { reachable } = await readPackagedApplicationGraph(appAsar);
|
||||
const reachableContents = Buffer.from(reachable.map(({ source }) => source).join('\n'));
|
||||
const trustSource = findPackagedMarketplaceTrustSource(reachable);
|
||||
if (!reachableContents.includes(Buffer.from(CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER))) {
|
||||
throw new Error('Packaged app.asar reachable graph does not contain the Marketplace trust provenance marker');
|
||||
}
|
||||
const verifiedTrustSource = `${trustSource}\n${CODE_OWNED_PLUGIN_SIGNING_KEYS_SOURCE_MARKER}`;
|
||||
return verifyMarketplaceClientArtifact(reachableContents, verifiedTrustSource);
|
||||
}
|
||||
|
||||
async function filesContainingNeedles(root, needles) {
|
||||
const matches = [];
|
||||
const visit = async (path) => {
|
||||
@@ -648,7 +688,6 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
|
||||
throw new Error(`Pi runtime manifest contains absolute paths: ${JSON.stringify(absoluteManifestValues)}`);
|
||||
}
|
||||
const appAsarContents = await readFile(appAsar);
|
||||
const marketplaceTrustSource = await readPackagedMarketplaceTrustSource(appAsar);
|
||||
const physicalOpenCodePaths = await collectForbiddenResourcePaths(resourcesDirectory);
|
||||
const asarOpenCodePaths = collectForbiddenAsarPaths(appAsar);
|
||||
const openCodeResourcePaths = classifyOpenCodeResourcePaths([
|
||||
@@ -666,7 +705,7 @@ export async function verifyPiProductArtifact({ projectRoot, executable }) {
|
||||
resourcesDirectory,
|
||||
appAsarContents,
|
||||
});
|
||||
const marketplace = verifyMarketplaceClientArtifact(appAsarContents, marketplaceTrustSource);
|
||||
const marketplace = await verifyPackagedMarketplaceClientArtifact(appAsar);
|
||||
const actualSkills = bundledPluginResources.coreResources.skills;
|
||||
const missingExtensionMarkers = EXTENSION_CONTRACT_MARKERS.filter(
|
||||
(marker) => !appAsarContents.includes(Buffer.from(marker)),
|
||||
|
||||
Reference in New Issue
Block a user