@extenshi/cli 0.0.7 → 0.0.9
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/dist/cli.js +108 -1
- package/dist/cli.js.map +1 -1
- package/dist/publish.d.ts +50 -0
- package/dist/publish.d.ts.map +1 -0
- package/dist/publish.js +178 -0
- package/dist/publish.js.map +1 -0
- package/dist/render.d.ts +21 -0
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +81 -1
- package/dist/render.js.map +1 -1
- package/dist/report.d.ts +16 -0
- package/dist/report.d.ts.map +1 -0
- package/dist/report.js +122 -0
- package/dist/report.js.map +1 -0
- package/dist/review-archive.d.ts +29 -0
- package/dist/review-archive.d.ts.map +1 -0
- package/dist/review-archive.js +106 -0
- package/dist/review-archive.js.map +1 -0
- package/dist/review-catalog.d.ts +43 -0
- package/dist/review-catalog.d.ts.map +1 -0
- package/dist/review-catalog.js +82 -0
- package/dist/review-catalog.js.map +1 -0
- package/dist/review-checks.d.ts +139 -0
- package/dist/review-checks.d.ts.map +1 -0
- package/dist/review-checks.js +560 -0
- package/dist/review-checks.js.map +1 -0
- package/dist/review-render.d.ts +10 -0
- package/dist/review-render.d.ts.map +1 -0
- package/dist/review-render.js +94 -0
- package/dist/review-render.js.map +1 -0
- package/dist/review-risk.d.ts +29 -0
- package/dist/review-risk.d.ts.map +1 -0
- package/dist/review-risk.js +85 -0
- package/dist/review-risk.js.map +1 -0
- package/dist/scan.d.ts +18 -1
- package/dist/scan.d.ts.map +1 -1
- package/dist/scan.js +45 -2
- package/dist/scan.js.map +1 -1
- package/package.json +3 -1
package/dist/report.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown store-compliance report renderer for `extenshi scan --report`.
|
|
3
|
+
*
|
|
4
|
+
* Pure (no I/O) so it is snapshot-testable. Schema-tolerant: reads whatever the
|
|
5
|
+
* scan server returns. The store-compliance metadata (label / storeImpact /
|
|
6
|
+
* remediation) is SERVER-DRIVEN — embedded in the `compliance` payload key — so
|
|
7
|
+
* the published CLI does not need to vendor the rule registry. When the server
|
|
8
|
+
* has not been updated to emit `compliance` yet, the report degrades to a note.
|
|
9
|
+
*/
|
|
10
|
+
import { STORE_IMPACT_SHORT, summarizeCompliance, summarizeReport, } from './render.js';
|
|
11
|
+
/** Escape characters that would break a markdown table cell. */
|
|
12
|
+
function cell(s) {
|
|
13
|
+
return s.replace(/\|/g, '\\|').replace(/\r?\n/g, ' ').trim();
|
|
14
|
+
}
|
|
15
|
+
const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'INFO', 'UNKNOWN'];
|
|
16
|
+
function computeVerdict(compliance, summary) {
|
|
17
|
+
const active = compliance.findings;
|
|
18
|
+
const hasRejection = active.some((f) => f.storeImpact === 'REJECTION_LIKELY');
|
|
19
|
+
const critHighSecurity = summary.critHigh > 0;
|
|
20
|
+
if (hasRejection) {
|
|
21
|
+
return {
|
|
22
|
+
headline: 'HIGH RISK of store rejection or delisting',
|
|
23
|
+
detail: 'This build has a manifest signal that stores commonly reject or delist for. Fix the high-impact items below before you submit.',
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
if (active.length > 0 || critHighSecurity) {
|
|
27
|
+
return {
|
|
28
|
+
headline: 'Review before submitting',
|
|
29
|
+
detail: 'Nothing here is an automatic rejection, but the items below commonly draw extra store scrutiny. Make sure each is justified in your listing.',
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
headline: 'No store-compliance blockers detected',
|
|
34
|
+
detail: 'Based on this scan, we did not find manifest signals that typically fail store review. This is not a guarantee of approval.',
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
function renderComplianceSection(compliance) {
|
|
38
|
+
const out = ['## Store-compliance risks (ranked by review danger)', ''];
|
|
39
|
+
if (!compliance.present) {
|
|
40
|
+
out.push('_Store-compliance analysis is unavailable — the scan server has not been updated yet. Re-run after the next server rollout (no CLI action needed)._', '');
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
if (compliance.findings.length === 0) {
|
|
44
|
+
out.push('_No store-compliance signals detected in the manifest._', '');
|
|
45
|
+
return out;
|
|
46
|
+
}
|
|
47
|
+
out.push('| # | Risk | Severity | Store impact | Stores |', '|---|------|----------|--------------|--------|');
|
|
48
|
+
compliance.findings.forEach((f, i) => {
|
|
49
|
+
const impact = STORE_IMPACT_SHORT[f.storeImpact] ?? f.storeImpact;
|
|
50
|
+
out.push(`| ${i + 1} | ${cell(f.label)} | ${cell(f.severity)} | ${cell(impact)} | ${cell(f.stores.join(', '))} |`);
|
|
51
|
+
});
|
|
52
|
+
out.push('');
|
|
53
|
+
compliance.findings.forEach((f, i) => {
|
|
54
|
+
out.push(`### ${i + 1}. ${f.label}`);
|
|
55
|
+
if (f.summary)
|
|
56
|
+
out.push('', `**Why stores care:** ${f.summary}`);
|
|
57
|
+
if (f.evidence.length > 0) {
|
|
58
|
+
out.push('', '**Evidence:**', '', '```', ...f.evidence.slice(0, 10), '```');
|
|
59
|
+
}
|
|
60
|
+
if (f.remediation)
|
|
61
|
+
out.push('', `**How to fix:** ${f.remediation}`);
|
|
62
|
+
out.push('');
|
|
63
|
+
});
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
function renderSecuritySection(summary) {
|
|
67
|
+
const out = ['## Security findings', ''];
|
|
68
|
+
if (summary.findings.length === 0) {
|
|
69
|
+
out.push('_No code-analysis findings._', '');
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
const bySeverity = new Map();
|
|
73
|
+
for (const f of summary.findings) {
|
|
74
|
+
const arr = bySeverity.get(f.severity) ?? [];
|
|
75
|
+
arr.push(f);
|
|
76
|
+
bySeverity.set(f.severity, arr);
|
|
77
|
+
}
|
|
78
|
+
for (const sev of SEVERITY_ORDER) {
|
|
79
|
+
const group = bySeverity.get(sev);
|
|
80
|
+
if (!group || group.length === 0)
|
|
81
|
+
continue;
|
|
82
|
+
out.push(`### ${sev} (${group.length})`, '', '| Finding | Scanner | Location |', '|---------|---------|----------|');
|
|
83
|
+
for (const f of group) {
|
|
84
|
+
out.push(`| ${cell(f.headline)} | ${cell(f.scanner)} | ${cell(f.location)} |`);
|
|
85
|
+
}
|
|
86
|
+
out.push('');
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
export function renderMarkdownReport(body, meta) {
|
|
91
|
+
const summary = summarizeReport(body);
|
|
92
|
+
const compliance = summarizeCompliance(body);
|
|
93
|
+
const lines = [];
|
|
94
|
+
lines.push(`# Extenshi scan report — ${meta.artifactName}`);
|
|
95
|
+
const job = summary.jobId ? ` · Job \`${summary.jobId}\`` : '';
|
|
96
|
+
lines.push(`> Generated by @extenshi/cli v${meta.cliVersion} · ${meta.generatedAt.toISOString()}${job}`, '');
|
|
97
|
+
const verdict = computeVerdict(compliance, summary);
|
|
98
|
+
lines.push('## Verdict', '', `**${verdict.headline}**`, '', verdict.detail, '');
|
|
99
|
+
lines.push(...renderComplianceSection(compliance));
|
|
100
|
+
if (compliance.present) {
|
|
101
|
+
lines.push('## Manifest risk score', '', `**${compliance.manifestRiskScore}/100**`, '');
|
|
102
|
+
if (compliance.flags.length > 0) {
|
|
103
|
+
lines.push(`Flags set: ${compliance.flags.map((f) => `\`${f}\``).join(', ')}`, '');
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
lines.push(...renderSecuritySection(summary));
|
|
107
|
+
lines.push('## Scanner status', '');
|
|
108
|
+
lines.push(`${summary.scannersOk}/${summary.scannersRun} scanners succeeded.`, '');
|
|
109
|
+
if (summary.issues.length > 0) {
|
|
110
|
+
lines.push('| Scanner | Status | Detail |', '|---------|--------|--------|');
|
|
111
|
+
for (const i of summary.issues) {
|
|
112
|
+
lines.push(`| ${cell(i.scanner)} | ${cell(i.status)} | ${cell(i.error ?? '')} |`);
|
|
113
|
+
}
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
lines.push('## Notes', '');
|
|
117
|
+
if (summary.jobId)
|
|
118
|
+
lines.push(`- Scan job: \`${summary.jobId}\``);
|
|
119
|
+
lines.push('- This scan is automated. The scanners are heuristics that may miss real issues or over-flag benign code — a clean result is not a guarantee of safety or store approval. See the [security-risk disclaimer](https://catalog.extenshi.io/disclaimers/security-risk).', '- If a credit was charged for a failed scan it auto-refunds within a minute.', '- Manage claims and respond to findings at https://dojo.extenshi.io', '');
|
|
120
|
+
return `${lines.join('\n')}\n`;
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"report.js","sourceRoot":"","sources":["../src/report.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — pull the manifest and inspectable text files out of a packaged
|
|
3
|
+
* extension artifact.
|
|
4
|
+
*
|
|
5
|
+
* Accepts the same artifacts as `extenshi scan`:
|
|
6
|
+
* .zip / .xpi → a plain ZIP
|
|
7
|
+
* .crx → a ZIP behind a CRX2 or CRX3 header (stripped here)
|
|
8
|
+
*
|
|
9
|
+
* Only text entries the checks read (.js/.mjs/.cjs/.html/.json) are decoded,
|
|
10
|
+
* each capped in size, with a global byte budget — a malicious or huge package
|
|
11
|
+
* can't blow up memory. Binary assets (images, wasm) are skipped.
|
|
12
|
+
*/
|
|
13
|
+
import type { FileMap, Manifest } from './review-checks.js';
|
|
14
|
+
export interface LoadedArtifact {
|
|
15
|
+
manifest: Manifest;
|
|
16
|
+
files: FileMap;
|
|
17
|
+
/** Non-fatal notes (e.g. byte budget hit, files skipped). */
|
|
18
|
+
warnings: string[];
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Strip a CRX header if present and return the inner ZIP bytes.
|
|
22
|
+
*
|
|
23
|
+
* CRX3: "Cr24" | version(4 LE)=3 | headerLen(4 LE) | header | ZIP
|
|
24
|
+
* CRX2: "Cr24" | version(4 LE)=2 | pubKeyLen(4 LE) | sigLen(4 LE) | pubKey | sig | ZIP
|
|
25
|
+
* Anything else is assumed to already be a ZIP.
|
|
26
|
+
*/
|
|
27
|
+
export declare function stripCrxHeader(buf: Buffer): Buffer;
|
|
28
|
+
export declare function loadArtifact(buf: Buffer): Promise<LoadedArtifact>;
|
|
29
|
+
//# sourceMappingURL=review-archive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-archive.d.ts","sourceRoot":"","sources":["../src/review-archive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAA;AAS3D,MAAM,WAAW,cAAc;IAC9B,QAAQ,EAAE,QAAQ,CAAA;IAClB,KAAK,EAAE,OAAO,CAAA;IACd,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,EAAE,CAAA;CAClB;AAED;;;;;;GAMG;AACH,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAelD;AAED,wBAAsB,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAuDvE"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — pull the manifest and inspectable text files out of a packaged
|
|
3
|
+
* extension artifact.
|
|
4
|
+
*
|
|
5
|
+
* Accepts the same artifacts as `extenshi scan`:
|
|
6
|
+
* .zip / .xpi → a plain ZIP
|
|
7
|
+
* .crx → a ZIP behind a CRX2 or CRX3 header (stripped here)
|
|
8
|
+
*
|
|
9
|
+
* Only text entries the checks read (.js/.mjs/.cjs/.html/.json) are decoded,
|
|
10
|
+
* each capped in size, with a global byte budget — a malicious or huge package
|
|
11
|
+
* can't blow up memory. Binary assets (images, wasm) are skipped.
|
|
12
|
+
*/
|
|
13
|
+
import JSZip from 'jszip';
|
|
14
|
+
/** Skip any single entry larger than this — source files are never this big. */
|
|
15
|
+
const MAX_ENTRY_BYTES = 4 * 1024 * 1024; // 4 MB
|
|
16
|
+
/** Stop decoding once the cumulative decoded text passes this. */
|
|
17
|
+
const MAX_TOTAL_BYTES = 40 * 1024 * 1024; // 40 MB
|
|
18
|
+
const TEXT_ENTRY_RE = /\.(?:m?js|cjs|html?|json)$/i;
|
|
19
|
+
/**
|
|
20
|
+
* Strip a CRX header if present and return the inner ZIP bytes.
|
|
21
|
+
*
|
|
22
|
+
* CRX3: "Cr24" | version(4 LE)=3 | headerLen(4 LE) | header | ZIP
|
|
23
|
+
* CRX2: "Cr24" | version(4 LE)=2 | pubKeyLen(4 LE) | sigLen(4 LE) | pubKey | sig | ZIP
|
|
24
|
+
* Anything else is assumed to already be a ZIP.
|
|
25
|
+
*/
|
|
26
|
+
export function stripCrxHeader(buf) {
|
|
27
|
+
if (buf.length < 16 || buf.toString('ascii', 0, 4) !== 'Cr24')
|
|
28
|
+
return buf;
|
|
29
|
+
const version = buf.readUInt32LE(4);
|
|
30
|
+
if (version === 3) {
|
|
31
|
+
const headerLen = buf.readUInt32LE(8);
|
|
32
|
+
const start = 12 + headerLen;
|
|
33
|
+
return start <= buf.length ? buf.subarray(start) : buf;
|
|
34
|
+
}
|
|
35
|
+
if (version === 2) {
|
|
36
|
+
const pubKeyLen = buf.readUInt32LE(8);
|
|
37
|
+
const sigLen = buf.readUInt32LE(12);
|
|
38
|
+
const start = 16 + pubKeyLen + sigLen;
|
|
39
|
+
return start <= buf.length ? buf.subarray(start) : buf;
|
|
40
|
+
}
|
|
41
|
+
return buf;
|
|
42
|
+
}
|
|
43
|
+
export async function loadArtifact(buf) {
|
|
44
|
+
const zip = await JSZip.loadAsync(stripCrxHeader(buf));
|
|
45
|
+
const manifestEntry = findManifestEntry(zip);
|
|
46
|
+
if (!manifestEntry) {
|
|
47
|
+
throw new Error('No manifest.json found in the artifact — is this a packaged extension (.zip/.crx/.xpi)?');
|
|
48
|
+
}
|
|
49
|
+
let manifest;
|
|
50
|
+
try {
|
|
51
|
+
manifest = JSON.parse(await manifestEntry.async('string'));
|
|
52
|
+
}
|
|
53
|
+
catch (err) {
|
|
54
|
+
throw new Error(`manifest.json is not valid JSON: ${err.message}`);
|
|
55
|
+
}
|
|
56
|
+
const files = new Map();
|
|
57
|
+
const warnings = [];
|
|
58
|
+
let totalBytes = 0;
|
|
59
|
+
let skippedLarge = 0;
|
|
60
|
+
let budgetHit = false;
|
|
61
|
+
for (const entry of Object.values(zip.files)) {
|
|
62
|
+
if (entry.dir)
|
|
63
|
+
continue;
|
|
64
|
+
if (!TEXT_ENTRY_RE.test(entry.name))
|
|
65
|
+
continue;
|
|
66
|
+
// jszip exposes the uncompressed size on the internal `_data` record; guard
|
|
67
|
+
// on it when available so we never decode a giant entry. This reaches into
|
|
68
|
+
// a private field — verified against jszip 3.10.x (the pinned `^3.10.1`).
|
|
69
|
+
// If you bump jszip, re-check that `_data.uncompressedSize` still exists;
|
|
70
|
+
// the text-length fallback below is the safety net if it ever disappears.
|
|
71
|
+
const declaredSize = entry._data
|
|
72
|
+
?.uncompressedSize;
|
|
73
|
+
if (typeof declaredSize === 'number' && declaredSize > MAX_ENTRY_BYTES) {
|
|
74
|
+
skippedLarge++;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (totalBytes >= MAX_TOTAL_BYTES) {
|
|
78
|
+
budgetHit = true;
|
|
79
|
+
break;
|
|
80
|
+
}
|
|
81
|
+
const text = await entry.async('string');
|
|
82
|
+
if (text.length > MAX_ENTRY_BYTES) {
|
|
83
|
+
skippedLarge++;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
totalBytes += text.length;
|
|
87
|
+
files.set(entry.name, text);
|
|
88
|
+
}
|
|
89
|
+
if (skippedLarge > 0)
|
|
90
|
+
warnings.push(`${skippedLarge} oversized file(s) skipped (>4 MB)`);
|
|
91
|
+
if (budgetHit)
|
|
92
|
+
warnings.push('file scan stopped early at the 40 MB decode budget — some files were not inspected');
|
|
93
|
+
return { manifest, files, warnings };
|
|
94
|
+
}
|
|
95
|
+
/** manifest.json lives at the package root; tolerate a stray leading folder just in case. */
|
|
96
|
+
function findManifestEntry(zip) {
|
|
97
|
+
const direct = zip.file('manifest.json');
|
|
98
|
+
if (direct)
|
|
99
|
+
return direct;
|
|
100
|
+
for (const entry of Object.values(zip.files)) {
|
|
101
|
+
if (!entry.dir && /(^|\/)manifest\.json$/i.test(entry.name))
|
|
102
|
+
return entry;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
//# sourceMappingURL=review-archive.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-archive.js","sourceRoot":"","sources":["../src/review-archive.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — fetch the last-published manifest for an extension from the
|
|
3
|
+
* Extenshi catalog so check 3 can diff against it. This is the part that is
|
|
4
|
+
* unique to us: we keep per-extension manifest snapshots, so we can tell you
|
|
5
|
+
* whether *this* update adds an auto-disabling permission versus what's live.
|
|
6
|
+
*
|
|
7
|
+
* Talks to the public BFF tRPC gateway:
|
|
8
|
+
* GET {bffUrl}/api/trpc/store.getLatestPublishedManifest?input=<json>
|
|
9
|
+
*
|
|
10
|
+
* The BFF read path may be gated behind a developer API key, so we send the
|
|
11
|
+
* `ek_` key when the caller has one. Everything here is fail-soft: any problem
|
|
12
|
+
* (key required, extension unknown, catalog older than this CLI, network error)
|
|
13
|
+
* resolves to `{ manifest: null, reason }` — the command then runs every other
|
|
14
|
+
* check and prints why the diff was skipped, rather than failing the run.
|
|
15
|
+
*/
|
|
16
|
+
export interface PriorManifestResult {
|
|
17
|
+
/** The parsed last-published manifest, or null when it could not be obtained. */
|
|
18
|
+
manifest: Record<string, unknown> | null;
|
|
19
|
+
/** Present when manifest is null: a human-readable reason to print. */
|
|
20
|
+
reason?: string;
|
|
21
|
+
/** Snapshot metadata, when found. */
|
|
22
|
+
meta?: {
|
|
23
|
+
store?: string;
|
|
24
|
+
version?: string;
|
|
25
|
+
fetchedAt?: string;
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export interface FetchPriorManifestOptions {
|
|
29
|
+
/** BFF base URL, e.g. https://bff.extenshi.io (no trailing slash). */
|
|
30
|
+
bffUrl: string;
|
|
31
|
+
/** Numeric catalog extension ID. */
|
|
32
|
+
extensionId: string;
|
|
33
|
+
/** Optional `ek_` developer key — sent when the read path is gated. */
|
|
34
|
+
apiKey?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Which store's snapshot to diff against. Permission sets differ per store, so
|
|
37
|
+
* pinning this avoids comparing a Chrome upload against, say, a Firefox
|
|
38
|
+
* snapshot. Omitted → the catalog returns the latest snapshot of any store.
|
|
39
|
+
*/
|
|
40
|
+
store?: 'CHROME' | 'FIREFOX' | 'EDGE';
|
|
41
|
+
}
|
|
42
|
+
export declare function fetchPriorManifest(opts: FetchPriorManifestOptions): Promise<PriorManifestResult>;
|
|
43
|
+
//# sourceMappingURL=review-catalog.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-catalog.d.ts","sourceRoot":"","sources":["../src/review-catalog.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAMH,MAAM,WAAW,mBAAmB;IACnC,iFAAiF;IACjF,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CAAA;IACxC,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf,qCAAqC;IACrC,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CAC/D;AAED,MAAM,WAAW,yBAAyB;IACzC,sEAAsE;IACtE,MAAM,EAAE,MAAM,CAAA;IACd,oCAAoC;IACpC,WAAW,EAAE,MAAM,CAAA;IACnB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,KAAK,CAAC,EAAE,QAAQ,GAAG,SAAS,GAAG,MAAM,CAAA;CACrC;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,yBAAyB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAiFtG"}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — fetch the last-published manifest for an extension from the
|
|
3
|
+
* Extenshi catalog so check 3 can diff against it. This is the part that is
|
|
4
|
+
* unique to us: we keep per-extension manifest snapshots, so we can tell you
|
|
5
|
+
* whether *this* update adds an auto-disabling permission versus what's live.
|
|
6
|
+
*
|
|
7
|
+
* Talks to the public BFF tRPC gateway:
|
|
8
|
+
* GET {bffUrl}/api/trpc/store.getLatestPublishedManifest?input=<json>
|
|
9
|
+
*
|
|
10
|
+
* The BFF read path may be gated behind a developer API key, so we send the
|
|
11
|
+
* `ek_` key when the caller has one. Everything here is fail-soft: any problem
|
|
12
|
+
* (key required, extension unknown, catalog older than this CLI, network error)
|
|
13
|
+
* resolves to `{ manifest: null, reason }` — the command then runs every other
|
|
14
|
+
* check and prints why the diff was skipped, rather than failing the run.
|
|
15
|
+
*/
|
|
16
|
+
import { fetch } from 'undici';
|
|
17
|
+
const FETCH_TIMEOUT_MS = 15_000;
|
|
18
|
+
export async function fetchPriorManifest(opts) {
|
|
19
|
+
const id = Number(opts.extensionId);
|
|
20
|
+
if (!Number.isInteger(id) || id <= 0) {
|
|
21
|
+
return {
|
|
22
|
+
manifest: null,
|
|
23
|
+
reason: `invalid --extension-id "${opts.extensionId}" (expected a positive number)`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
const query = opts.store ? { extensionId: id, store: opts.store } : { extensionId: id };
|
|
27
|
+
const input = encodeURIComponent(JSON.stringify(query));
|
|
28
|
+
const url = `${opts.bffUrl}/api/trpc/store.getLatestPublishedManifest?input=${input}`;
|
|
29
|
+
let res;
|
|
30
|
+
try {
|
|
31
|
+
res = await fetch(url, {
|
|
32
|
+
headers: opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
|
|
33
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
catch (err) {
|
|
37
|
+
const e = err;
|
|
38
|
+
const why = e.name === 'TimeoutError' ? `timed out after ${FETCH_TIMEOUT_MS / 1000}s` : e.message;
|
|
39
|
+
return { manifest: null, reason: `could not reach the catalog (${why})` };
|
|
40
|
+
}
|
|
41
|
+
if (res.status === 401) {
|
|
42
|
+
return {
|
|
43
|
+
manifest: null,
|
|
44
|
+
reason: 'the catalog manifest read is gated — set EXTENSHI_API_KEY (run `extenshi login`) to enable the diff',
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (res.status === 404) {
|
|
48
|
+
return {
|
|
49
|
+
manifest: null,
|
|
50
|
+
reason: 'the deployed catalog API predates this feature (no getLatestPublishedManifest) — update once it ships',
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
if (!res.ok) {
|
|
54
|
+
return { manifest: null, reason: `catalog returned HTTP ${res.status}` };
|
|
55
|
+
}
|
|
56
|
+
let body;
|
|
57
|
+
try {
|
|
58
|
+
body = await res.json();
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return { manifest: null, reason: 'catalog returned a non-JSON response' };
|
|
62
|
+
}
|
|
63
|
+
// tRPC (no transformer): { result: { data: <payload> } }, or { error: {...} }.
|
|
64
|
+
const error = body.error;
|
|
65
|
+
if (error) {
|
|
66
|
+
return { manifest: null, reason: `catalog error: ${error.message ?? 'unknown'}` };
|
|
67
|
+
}
|
|
68
|
+
const data = body.result?.data;
|
|
69
|
+
if (!data || data.manifest == null || typeof data.manifest !== 'object') {
|
|
70
|
+
const reason = data?.reason === 'no_snapshot'
|
|
71
|
+
? 'no published manifest on record for this extension yet'
|
|
72
|
+
: data?.reason === 'manifest_unresolved'
|
|
73
|
+
? 'the last snapshot could not be resolved to a manifest'
|
|
74
|
+
: 'the catalog returned no manifest for this extension';
|
|
75
|
+
return { manifest: null, reason };
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
manifest: data.manifest,
|
|
79
|
+
meta: { store: data.store, version: data.version, fetchedAt: data.fetchedAt },
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=review-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-catalog.js","sourceRoot":"","sources":["../src/review-catalog.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-risk — pure check logic for the pre-publish "will the store reject /
|
|
3
|
+
* slow-walk / silently disable this?" suite.
|
|
4
|
+
*
|
|
5
|
+
* Every finding maps to ONE of three real-world consequences, never a generic
|
|
6
|
+
* lint level:
|
|
7
|
+
*
|
|
8
|
+
* REJECTED — the store will reject the submission (you cannot ship as-is).
|
|
9
|
+
* SLOW — it ships, but review moves from <24h to several days.
|
|
10
|
+
* ATTRITION — it ships and passes review, but installed users are silently
|
|
11
|
+
* dropped on the update until they re-accept a new permission.
|
|
12
|
+
*
|
|
13
|
+
* Each check's `citation` points to the relevant official Chrome Web Store /
|
|
14
|
+
* Chrome for Developers documentation so the user can verify the rule at the
|
|
15
|
+
* source. (We do not cite third-party books in product output.)
|
|
16
|
+
*
|
|
17
|
+
* This module is PURE (no I/O) so the rules are unit-testable against fixtures.
|
|
18
|
+
* Archive extraction lives in review-archive.ts; the network manifest fetch in
|
|
19
|
+
* review-catalog.ts; rendering in review-render.ts.
|
|
20
|
+
*/
|
|
21
|
+
export type Verdict = 'REJECTED' | 'SLOW' | 'ATTRITION';
|
|
22
|
+
export interface ReviewFinding {
|
|
23
|
+
/** The real-world consequence — drives the label shown to the user. */
|
|
24
|
+
verdict: Verdict;
|
|
25
|
+
/** Stable machine id (for --json consumers / suppression later). */
|
|
26
|
+
check: string;
|
|
27
|
+
/** One-line headline. */
|
|
28
|
+
title: string;
|
|
29
|
+
/** Why it matters, in plain language. */
|
|
30
|
+
detail: string;
|
|
31
|
+
/** The offending value or `file:line`, when there is one. */
|
|
32
|
+
evidence?: string;
|
|
33
|
+
/** The concrete fix. */
|
|
34
|
+
recommendation: string;
|
|
35
|
+
/** Official Chrome documentation backing the rule (shown to the user). */
|
|
36
|
+
citation: string;
|
|
37
|
+
}
|
|
38
|
+
/** A parsed manifest.json — only the fields the checks read are typed. */
|
|
39
|
+
export interface Manifest {
|
|
40
|
+
manifest_version?: number;
|
|
41
|
+
name?: string;
|
|
42
|
+
version?: string;
|
|
43
|
+
permissions?: unknown;
|
|
44
|
+
optional_permissions?: unknown;
|
|
45
|
+
host_permissions?: unknown;
|
|
46
|
+
optional_host_permissions?: unknown;
|
|
47
|
+
content_scripts?: Array<{
|
|
48
|
+
matches?: unknown;
|
|
49
|
+
js?: unknown;
|
|
50
|
+
[k: string]: unknown;
|
|
51
|
+
}>;
|
|
52
|
+
background?: {
|
|
53
|
+
service_worker?: string;
|
|
54
|
+
scripts?: unknown;
|
|
55
|
+
page?: string;
|
|
56
|
+
type?: string;
|
|
57
|
+
[k: string]: unknown;
|
|
58
|
+
};
|
|
59
|
+
action?: {
|
|
60
|
+
default_popup?: string;
|
|
61
|
+
[k: string]: unknown;
|
|
62
|
+
};
|
|
63
|
+
browser_action?: {
|
|
64
|
+
default_popup?: string;
|
|
65
|
+
[k: string]: unknown;
|
|
66
|
+
};
|
|
67
|
+
options_ui?: {
|
|
68
|
+
page?: string;
|
|
69
|
+
[k: string]: unknown;
|
|
70
|
+
};
|
|
71
|
+
options_page?: string;
|
|
72
|
+
side_panel?: {
|
|
73
|
+
default_path?: string;
|
|
74
|
+
[k: string]: unknown;
|
|
75
|
+
};
|
|
76
|
+
devtools_page?: string;
|
|
77
|
+
chrome_url_overrides?: Record<string, string>;
|
|
78
|
+
sandbox?: {
|
|
79
|
+
pages?: unknown;
|
|
80
|
+
[k: string]: unknown;
|
|
81
|
+
};
|
|
82
|
+
[k: string]: unknown;
|
|
83
|
+
}
|
|
84
|
+
/** A text file pulled out of the artifact: package-relative path → contents. */
|
|
85
|
+
export type FileMap = Map<string, string>;
|
|
86
|
+
export declare const PERMISSION_WARNINGS: Readonly<Record<string, string>>;
|
|
87
|
+
export declare function isGlobalHost(value: string): boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Required API permissions only (host patterns stripped out). In MV2 hosts and
|
|
90
|
+
* API permissions share the `permissions` array, so we split by shape.
|
|
91
|
+
*/
|
|
92
|
+
export declare function requiredApiPermissions(m: Manifest): string[];
|
|
93
|
+
/** Required host patterns: `host_permissions` (MV3) plus any host-shaped entry in `permissions` (MV2). */
|
|
94
|
+
export declare function requiredHostPermissions(m: Manifest): string[];
|
|
95
|
+
/** Normalise a manifest-declared path to the package-relative key used in the file map. */
|
|
96
|
+
export declare function normalizePath(p: string): string;
|
|
97
|
+
/**
|
|
98
|
+
* HTML pages the browser renders inside the extension's own CSP — popup,
|
|
99
|
+
* options, side panel, devtools, newtab/history/bookmarks overrides, sandbox
|
|
100
|
+
* pages, and the MV2 background page. These are exactly where inline scripts and
|
|
101
|
+
* eval get the extension rejected.
|
|
102
|
+
*/
|
|
103
|
+
export declare function htmlEntrypoints(m: Manifest): string[];
|
|
104
|
+
export declare function checkSlowReviewQueue(m: Manifest): ReviewFinding[];
|
|
105
|
+
/**
|
|
106
|
+
* Static advisory — there is nothing in the package to inspect; it's a
|
|
107
|
+
* publishing-process fact developers routinely get burned by. Emitted once.
|
|
108
|
+
*/
|
|
109
|
+
export declare function checkListingChangeReReview(): ReviewFinding[];
|
|
110
|
+
/**
|
|
111
|
+
* Diff the local manifest against the last-published one. A newly-added
|
|
112
|
+
* *required* permission that shows a warning silently disables the extension for
|
|
113
|
+
* the whole installed base on update (only a small toolbar icon appears) →
|
|
114
|
+
* silent attrition. Newly-added permissions of any kind also re-trip the slow
|
|
115
|
+
* review queue (ch.10).
|
|
116
|
+
*
|
|
117
|
+
* `prior` is the manifest fetched from the catalog's last snapshot; pass null
|
|
118
|
+
* when it could not be fetched (the caller surfaces why and this returns []).
|
|
119
|
+
*/
|
|
120
|
+
export declare function checkAutoDisableOnUpdate(local: Manifest, prior: Manifest | null): ReviewFinding[];
|
|
121
|
+
export declare function checkRemoteCode(files: FileMap): ReviewFinding[];
|
|
122
|
+
export declare function checkHtmlCsp(m: Manifest, files: FileMap): ReviewFinding[];
|
|
123
|
+
export declare function checkServiceWorker(m: Manifest, files: FileMap): ReviewFinding[];
|
|
124
|
+
export declare function checkActiveTabAdvisor(m: Manifest): ReviewFinding[];
|
|
125
|
+
export interface ReviewInputs {
|
|
126
|
+
manifest: Manifest;
|
|
127
|
+
files: FileMap;
|
|
128
|
+
/** Last-published manifest from the catalog, or null when unavailable. */
|
|
129
|
+
priorManifest: Manifest | null;
|
|
130
|
+
}
|
|
131
|
+
export declare function runChecks(input: ReviewInputs): ReviewFinding[];
|
|
132
|
+
export interface ReviewSummary {
|
|
133
|
+
findings: ReviewFinding[];
|
|
134
|
+
counts: Record<Verdict, number>;
|
|
135
|
+
/** True when at least one finding will block publishing. */
|
|
136
|
+
blocking: boolean;
|
|
137
|
+
}
|
|
138
|
+
export declare function summarize(findings: ReviewFinding[]): ReviewSummary;
|
|
139
|
+
//# sourceMappingURL=review-checks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-checks.d.ts","sourceRoot":"","sources":["../src/review-checks.ts"],"names":[],"mappings":"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"}
|