@extenshi/cli 0.0.1
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/auto-update.d.ts +15 -0
- package/dist/auto-update.d.ts.map +1 -0
- package/dist/auto-update.js +100 -0
- package/dist/auto-update.js.map +1 -0
- package/dist/cli.d.ts +19 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +67 -0
- package/dist/cli.js.map +1 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +30 -0
- package/dist/config.js.map +1 -0
- package/dist/render.d.ts +10 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +127 -0
- package/dist/render.js.map +1 -0
- package/dist/scan.d.ts +34 -0
- package/dist/scan.d.ts.map +1 -0
- package/dist/scan.js +131 -0
- package/dist/scan.js.map +1 -0
- package/package.json +37 -0
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update check for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* On each run:
|
|
5
|
+
* 1. Check ~/.extenshi/last-update-check.json (24h TTL).
|
|
6
|
+
* 2. If stale, fetch https://registry.npmjs.org/@extenshi/cli/latest (2s timeout, fail-soft).
|
|
7
|
+
* 3. Compare current vs latest via semver.diff:
|
|
8
|
+
* - major → FATAL: print upgrade message and exit 1
|
|
9
|
+
* - minor/patch → warn to stderr, continue
|
|
10
|
+
*
|
|
11
|
+
* Fail-soft: any network error or parse failure is silently ignored so the
|
|
12
|
+
* scan itself is never blocked by a registry outage.
|
|
13
|
+
*/
|
|
14
|
+
export declare function checkAutoUpdate(): Promise<void>;
|
|
15
|
+
//# sourceMappingURL=auto-update.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-update.d.ts","sourceRoot":"","sources":["../src/auto-update.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AA4DH,wBAAsB,eAAe,IAAI,OAAO,CAAC,IAAI,CAAC,CAuCrD"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update check for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* On each run:
|
|
5
|
+
* 1. Check ~/.extenshi/last-update-check.json (24h TTL).
|
|
6
|
+
* 2. If stale, fetch https://registry.npmjs.org/@extenshi/cli/latest (2s timeout, fail-soft).
|
|
7
|
+
* 3. Compare current vs latest via semver.diff:
|
|
8
|
+
* - major → FATAL: print upgrade message and exit 1
|
|
9
|
+
* - minor/patch → warn to stderr, continue
|
|
10
|
+
*
|
|
11
|
+
* Fail-soft: any network error or parse failure is silently ignored so the
|
|
12
|
+
* scan itself is never blocked by a registry outage.
|
|
13
|
+
*/
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import { createRequire } from 'node:module';
|
|
16
|
+
import os from 'node:os';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import semver from 'semver';
|
|
19
|
+
const CACHE_PATH = path.join(os.homedir(), '.extenshi', 'last-update-check.json');
|
|
20
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
21
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/@extenshi/cli/latest';
|
|
22
|
+
const REGISTRY_TIMEOUT_MS = 2000;
|
|
23
|
+
function getCurrentVersion() {
|
|
24
|
+
// Read version from this package's package.json
|
|
25
|
+
try {
|
|
26
|
+
const require = createRequire(import.meta.url);
|
|
27
|
+
const pkg = require('../package.json');
|
|
28
|
+
return pkg.version;
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
return '1.0.0';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function readCache() {
|
|
35
|
+
try {
|
|
36
|
+
const raw = fs.readFileSync(CACHE_PATH, 'utf8');
|
|
37
|
+
return JSON.parse(raw);
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function writeCache(latestVersion) {
|
|
44
|
+
try {
|
|
45
|
+
fs.mkdirSync(path.dirname(CACHE_PATH), { recursive: true });
|
|
46
|
+
fs.writeFileSync(CACHE_PATH, JSON.stringify({ checkedAt: Date.now(), latestVersion }));
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
// Non-fatal
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
async function fetchLatestVersion() {
|
|
53
|
+
try {
|
|
54
|
+
const response = await fetch(REGISTRY_URL, {
|
|
55
|
+
signal: AbortSignal.timeout(REGISTRY_TIMEOUT_MS),
|
|
56
|
+
});
|
|
57
|
+
if (!response.ok)
|
|
58
|
+
return null;
|
|
59
|
+
const data = (await response.json());
|
|
60
|
+
return data.version ?? null;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null; // fail-soft: registry unavailable
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
export async function checkAutoUpdate() {
|
|
67
|
+
const currentVersion = getCurrentVersion();
|
|
68
|
+
let latestVersion = null;
|
|
69
|
+
const cache = readCache();
|
|
70
|
+
if (cache && Date.now() - cache.checkedAt < CACHE_TTL_MS) {
|
|
71
|
+
// Cache is fresh — use cached value
|
|
72
|
+
latestVersion = cache.latestVersion;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// Cache is stale (or missing) — fetch from registry
|
|
76
|
+
latestVersion = await fetchLatestVersion();
|
|
77
|
+
if (latestVersion) {
|
|
78
|
+
writeCache(latestVersion);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!latestVersion)
|
|
82
|
+
return; // couldn't determine latest — skip check
|
|
83
|
+
const diff = semver.diff(currentVersion, latestVersion);
|
|
84
|
+
if (!diff)
|
|
85
|
+
return; // same version or parse error
|
|
86
|
+
if (diff === 'major') {
|
|
87
|
+
// Fatal: major version incompatibility
|
|
88
|
+
console.error(`\n❌ extenshi CLI is outdated (major version mismatch).\n` +
|
|
89
|
+
` Current: v${currentVersion} Latest: v${latestVersion}\n\n` +
|
|
90
|
+
` Upgrade required:\n` +
|
|
91
|
+
` npm install -g @extenshi/cli@latest\n` +
|
|
92
|
+
` or:\n` +
|
|
93
|
+
` yarn global add @extenshi/cli@latest\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
// minor or patch — warn and continue
|
|
97
|
+
process.stderr.write(`⚠️ extenshi CLI update available: v${currentVersion} → v${latestVersion}\n` +
|
|
98
|
+
` Run: npm install -g @extenshi/cli@latest\n\n`);
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=auto-update.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auto-update.js","sourceRoot":"","sources":["../src/auto-update.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @extenshi/cli — pre-publish extension security scanner.
|
|
4
|
+
*
|
|
5
|
+
* Entry point. Parses CLI args via commander, runs auto-update check,
|
|
6
|
+
* then delegates to the scan command.
|
|
7
|
+
*
|
|
8
|
+
* CLI flow:
|
|
9
|
+
*
|
|
10
|
+
* extenshi scan <artifact>
|
|
11
|
+
* │
|
|
12
|
+
* ├─ load API key (env > config file)
|
|
13
|
+
* ├─ auto-update check (registry, 24h cache, fail-soft)
|
|
14
|
+
* ├─ CI timeout pre-flight (GET /api/v1/healthz?delay=65, timeout 70s)
|
|
15
|
+
* └─ POST /api/v1/scan (multipart, timeout 240s)
|
|
16
|
+
* └─ render findings (pretty or --json)
|
|
17
|
+
*/
|
|
18
|
+
export {};
|
|
19
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @extenshi/cli — pre-publish extension security scanner.
|
|
4
|
+
*
|
|
5
|
+
* Entry point. Parses CLI args via commander, runs auto-update check,
|
|
6
|
+
* then delegates to the scan command.
|
|
7
|
+
*
|
|
8
|
+
* CLI flow:
|
|
9
|
+
*
|
|
10
|
+
* extenshi scan <artifact>
|
|
11
|
+
* │
|
|
12
|
+
* ├─ load API key (env > config file)
|
|
13
|
+
* ├─ auto-update check (registry, 24h cache, fail-soft)
|
|
14
|
+
* ├─ CI timeout pre-flight (GET /api/v1/healthz?delay=65, timeout 70s)
|
|
15
|
+
* └─ POST /api/v1/scan (multipart, timeout 240s)
|
|
16
|
+
* └─ render findings (pretty or --json)
|
|
17
|
+
*/
|
|
18
|
+
import { createRequire } from 'node:module';
|
|
19
|
+
import { program } from 'commander';
|
|
20
|
+
import { checkAutoUpdate } from './auto-update.js';
|
|
21
|
+
import { loadConfig } from './config.js';
|
|
22
|
+
import { runScan } from './scan.js';
|
|
23
|
+
const _require = createRequire(import.meta.url);
|
|
24
|
+
const pkg = _require('../package.json');
|
|
25
|
+
program
|
|
26
|
+
.name('extenshi')
|
|
27
|
+
.description('Pre-publish security scanner for browser extensions')
|
|
28
|
+
.version(pkg.version);
|
|
29
|
+
program
|
|
30
|
+
.command('scan <artifact>')
|
|
31
|
+
.description('Scan a browser extension artifact (.crx, .xpi, or .zip)')
|
|
32
|
+
.option('--json', 'Output raw JSON (default: pretty-printed findings)')
|
|
33
|
+
.option('--extension-id <id>', 'Extension store ID (required for free-credit scans on claimed extensions)')
|
|
34
|
+
.option('--skip-preflight', 'Skip CI timeout pre-flight probe (use at your own risk)')
|
|
35
|
+
.option('--api-url <url>', 'Override the scan API base URL (default: https://scan.extenshi.io)')
|
|
36
|
+
.action(async (artifact, opts) => {
|
|
37
|
+
try {
|
|
38
|
+
const config = await loadConfig(opts.apiUrl);
|
|
39
|
+
if (!config.apiKey) {
|
|
40
|
+
console.error('Error: API key not found.\n' +
|
|
41
|
+
'Set EXTENSHI_API_KEY environment variable or run: extenshi login\n' +
|
|
42
|
+
'Get an API key at https://dojo.extenshi.io/billing/cli');
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
// Auto-update check (fail-soft — never blocks scan)
|
|
46
|
+
await checkAutoUpdate();
|
|
47
|
+
await runScan({
|
|
48
|
+
artifactPath: artifact,
|
|
49
|
+
apiKey: config.apiKey,
|
|
50
|
+
apiUrl: config.apiUrl,
|
|
51
|
+
extensionId: opts.extensionId,
|
|
52
|
+
outputJson: opts.json ?? false,
|
|
53
|
+
skipPreflight: opts.skipPreflight ?? false,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
if (err instanceof Error) {
|
|
58
|
+
console.error(`Error: ${err.message}`);
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
console.error('Unexpected error:', err);
|
|
62
|
+
}
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
program.parse();
|
|
67
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAA;AAC3C,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AACnC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA;AAClD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAA;AAEnC,MAAM,QAAQ,GAAG,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;AAC/C,MAAM,GAAG,GAAG,QAAQ,CAAC,iBAAiB,CAAwB,CAAA;AAE9D,OAAO;KACL,IAAI,CAAC,UAAU,CAAC;KAChB,WAAW,CAAC,qDAAqD,CAAC;KAClE,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;AAEtB,OAAO;KACL,OAAO,CAAC,iBAAiB,CAAC;KAC1B,WAAW,CAAC,yDAAyD,CAAC;KACtE,MAAM,CAAC,QAAQ,EAAE,oDAAoD,CAAC;KACtE,MAAM,CAAC,qBAAqB,EAAE,2EAA2E,CAAC;KAC1G,MAAM,CAAC,kBAAkB,EAAE,yDAAyD,CAAC;KACrF,MAAM,CAAC,iBAAiB,EAAE,oEAAoE,CAAC;KAC/F,MAAM,CACN,KAAK,EACJ,QAAgB,EAChB,IAKC,EACA,EAAE;IACH,IAAI,CAAC;QACJ,MAAM,MAAM,GAAG,MAAM,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QAE5C,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,CAAC;YACpB,OAAO,CAAC,KAAK,CACZ,6BAA6B;gBAC5B,oEAAoE;gBACpE,wDAAwD,CACzD,CAAA;YACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAChB,CAAC;QAED,oDAAoD;QACpD,MAAM,eAAe,EAAE,CAAA;QAEvB,MAAM,OAAO,CAAC;YACb,YAAY,EAAE,QAAQ;YACtB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,MAAM,EAAE,MAAM,CAAC,MAAM;YACrB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,UAAU,EAAE,IAAI,CAAC,IAAI,IAAI,KAAK;YAC9B,aAAa,EAAE,IAAI,CAAC,aAAa,IAAI,KAAK;SAC1C,CAAC,CAAA;IACH,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACd,IAAI,GAAG,YAAY,KAAK,EAAE,CAAC;YAC1B,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,OAAO,EAAE,CAAC,CAAA;QACvC,CAAC;aAAM,CAAC;YACP,OAAO,CAAC,KAAK,CAAC,mBAAmB,EAAE,GAAG,CAAC,CAAA;QACxC,CAAC;QACD,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAChB,CAAC;AACF,CAAC,CACD,CAAA;AAEF,OAAO,CAAC,KAAK,EAAE,CAAA"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* Priority order:
|
|
5
|
+
* 1. Environment variable EXTENSHI_API_KEY / EXTENSHI_API_URL
|
|
6
|
+
* 2. Config file ~/.extenshi/config.json
|
|
7
|
+
* 3. Hardcoded defaults
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadConfig(cliApiUrl?: string): Promise<{
|
|
10
|
+
apiKey: string | null;
|
|
11
|
+
apiUrl: string;
|
|
12
|
+
}>;
|
|
13
|
+
//# sourceMappingURL=config.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAuBH,wBAAsB,UAAU,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;IAAE,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAQvG"}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration loading for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* Priority order:
|
|
5
|
+
* 1. Environment variable EXTENSHI_API_KEY / EXTENSHI_API_URL
|
|
6
|
+
* 2. Config file ~/.extenshi/config.json
|
|
7
|
+
* 3. Hardcoded defaults
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import os from 'node:os';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
const DEFAULT_API_URL = 'https://scan.extenshi.io';
|
|
13
|
+
const CONFIG_PATH = path.join(os.homedir(), '.extenshi', 'config.json');
|
|
14
|
+
function readConfigFile() {
|
|
15
|
+
try {
|
|
16
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf8');
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return {};
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export async function loadConfig(cliApiUrl) {
|
|
24
|
+
const fileConfig = readConfigFile();
|
|
25
|
+
const apiKey = process.env.EXTENSHI_API_KEY ?? fileConfig.apiKey ?? null;
|
|
26
|
+
const apiUrl = cliApiUrl ?? process.env.EXTENSHI_API_URL ?? fileConfig.apiUrl ?? DEFAULT_API_URL;
|
|
27
|
+
// Strip trailing slash
|
|
28
|
+
return { apiKey, apiUrl: apiUrl.replace(/\/$/, '') };
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,EAAE,MAAM,SAAS,CAAA;AACxB,OAAO,IAAI,MAAM,WAAW,CAAA;AAE5B,MAAM,eAAe,GAAG,0BAA0B,CAAA;AAClD,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,aAAa,CAAC,CAAA;AAOvE,SAAS,cAAc;IACtB,IAAI,CAAC;QACJ,MAAM,GAAG,GAAG,EAAE,CAAC,YAAY,CAAC,WAAW,EAAE,MAAM,CAAC,CAAA;QAChD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAe,CAAA;IACrC,CAAC;IAAC,MAAM,CAAC;QACR,OAAO,EAAE,CAAA;IACV,CAAC;AACF,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAkB;IAClD,MAAM,UAAU,GAAG,cAAc,EAAE,CAAA;IAEnC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,IAAI,IAAI,CAAA;IACxE,MAAM,MAAM,GAAG,SAAS,IAAI,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,UAAU,CAAC,MAAM,IAAI,eAAe,CAAA;IAEhG,uBAAuB;IACvB,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,EAAE,CAAA;AACrD,CAAC"}
|
package/dist/render.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-print scan findings for terminal output.
|
|
3
|
+
*
|
|
4
|
+
* Groups findings by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO),
|
|
5
|
+
* then by scanner, using chalk for colour.
|
|
6
|
+
*
|
|
7
|
+
* Invoked only when --json flag is NOT set.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renderReport(body: Record<string, unknown>): void;
|
|
10
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.d.ts","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AA4DH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,IAAI,CA2GhE"}
|
package/dist/render.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-print scan findings for terminal output.
|
|
3
|
+
*
|
|
4
|
+
* Groups findings by severity (CRITICAL → HIGH → MEDIUM → LOW → INFO),
|
|
5
|
+
* then by scanner, using chalk for colour.
|
|
6
|
+
*
|
|
7
|
+
* Invoked only when --json flag is NOT set.
|
|
8
|
+
*/
|
|
9
|
+
import chalk from 'chalk';
|
|
10
|
+
// ── Severity ordering ──────────────────────────────────────────────────────────
|
|
11
|
+
const SEVERITY_ORDER = {
|
|
12
|
+
CRITICAL: 0,
|
|
13
|
+
HIGH: 1,
|
|
14
|
+
MEDIUM: 2,
|
|
15
|
+
LOW: 3,
|
|
16
|
+
INFO: 4,
|
|
17
|
+
};
|
|
18
|
+
function severityRank(s) {
|
|
19
|
+
return SEVERITY_ORDER[s?.toUpperCase() ?? ''] ?? 99;
|
|
20
|
+
}
|
|
21
|
+
function severityColour(s) {
|
|
22
|
+
switch (s?.toUpperCase()) {
|
|
23
|
+
case 'CRITICAL':
|
|
24
|
+
return chalk.bgRed.white.bold(` ${s?.toUpperCase()} `);
|
|
25
|
+
case 'HIGH':
|
|
26
|
+
return chalk.red.bold(s?.toUpperCase());
|
|
27
|
+
case 'MEDIUM':
|
|
28
|
+
return chalk.yellow.bold(s?.toUpperCase());
|
|
29
|
+
case 'LOW':
|
|
30
|
+
return chalk.cyan(s?.toUpperCase());
|
|
31
|
+
case 'INFO':
|
|
32
|
+
return chalk.dim(s?.toUpperCase() ?? 'INFO');
|
|
33
|
+
default:
|
|
34
|
+
return chalk.dim(s ?? 'UNKNOWN');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// ── Main render function ───────────────────────────────────────────────────────
|
|
38
|
+
export function renderReport(body) {
|
|
39
|
+
const report = body;
|
|
40
|
+
const scanners = report.scanners ?? [];
|
|
41
|
+
const allFindings = [];
|
|
42
|
+
let scannersRun = 0;
|
|
43
|
+
let scannersOk = 0;
|
|
44
|
+
for (const s of scanners) {
|
|
45
|
+
scannersRun++;
|
|
46
|
+
if (s.status === 'SUCCESS' || s.status === 'success') {
|
|
47
|
+
scannersOk++;
|
|
48
|
+
}
|
|
49
|
+
for (const f of s.findings ?? []) {
|
|
50
|
+
allFindings.push({ scanner: s.scanner ?? 'unknown', finding: f });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Sort by severity
|
|
54
|
+
allFindings.sort((a, b) => severityRank(a.finding.severity) - severityRank(b.finding.severity));
|
|
55
|
+
// Header
|
|
56
|
+
console.log();
|
|
57
|
+
if (allFindings.length === 0) {
|
|
58
|
+
console.log(chalk.green.bold('✓ No findings — extension looks clean.'));
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
const critHigh = allFindings.filter((f) => severityRank(f.finding.severity) <= 1).length;
|
|
62
|
+
const label = critHigh > 0
|
|
63
|
+
? chalk.red.bold(`${allFindings.length} finding${allFindings.length !== 1 ? 's' : ''} (${critHigh} critical/high)`)
|
|
64
|
+
: chalk.yellow.bold(`${allFindings.length} finding${allFindings.length !== 1 ? 's' : ''}`);
|
|
65
|
+
console.log(`${chalk.bold('Scan report')} · ${label}`);
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.dim(`Scanners: ${scannersOk}/${scannersRun} succeeded`));
|
|
68
|
+
if (report.jobId) {
|
|
69
|
+
console.log(chalk.dim(`Job: ${report.jobId}`));
|
|
70
|
+
}
|
|
71
|
+
console.log();
|
|
72
|
+
if (allFindings.length === 0) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Print findings grouped by severity (already sorted)
|
|
76
|
+
let lastSeverity = '';
|
|
77
|
+
for (const { scanner, finding } of allFindings) {
|
|
78
|
+
const sev = finding.severity?.toUpperCase() ?? 'UNKNOWN';
|
|
79
|
+
if (sev !== lastSeverity) {
|
|
80
|
+
lastSeverity = sev;
|
|
81
|
+
console.log(chalk.bold(`── ${severityColour(sev)} ──────────────────────────`));
|
|
82
|
+
}
|
|
83
|
+
const rule = finding.rule ? chalk.bold(finding.rule) : chalk.dim('(no rule)');
|
|
84
|
+
const desc = finding.description ?? '';
|
|
85
|
+
console.log(` ${rule} ${chalk.dim(`[${scanner}]`)}`);
|
|
86
|
+
if (desc) {
|
|
87
|
+
// Wrap description at 80 chars
|
|
88
|
+
const words = desc.split(' ');
|
|
89
|
+
let line = ' ';
|
|
90
|
+
for (const word of words) {
|
|
91
|
+
if (line.length + word.length + 1 > 80) {
|
|
92
|
+
console.log(chalk.dim(line));
|
|
93
|
+
line = ` ${word}`;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
line += (line === ' ' ? '' : ' ') + word;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (line.trim()) {
|
|
100
|
+
console.log(chalk.dim(line));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// Print any extra fields (except known ones we've already displayed)
|
|
104
|
+
const skip = new Set(['rule', 'description', 'severity']);
|
|
105
|
+
const extras = Object.entries(finding)
|
|
106
|
+
.filter(([k]) => !skip.has(k))
|
|
107
|
+
.map(([k, v]) => `${k}: ${JSON.stringify(v)}`);
|
|
108
|
+
if (extras.length > 0) {
|
|
109
|
+
console.log(chalk.dim(` ${extras.slice(0, 3).join(' ')}`));
|
|
110
|
+
}
|
|
111
|
+
console.log();
|
|
112
|
+
}
|
|
113
|
+
// Summary per scanner (statuses)
|
|
114
|
+
const failedScanners = scanners.filter((s) => s.status !== 'SUCCESS' && s.status !== 'success');
|
|
115
|
+
if (failedScanners.length > 0) {
|
|
116
|
+
console.log(chalk.dim('── Scanner issues ──────────────────────────'));
|
|
117
|
+
for (const s of failedScanners) {
|
|
118
|
+
console.log(chalk.dim(` ${s.scanner ?? 'unknown'}: ${s.status ?? 'unknown'}`));
|
|
119
|
+
}
|
|
120
|
+
console.log();
|
|
121
|
+
}
|
|
122
|
+
// Exit hint
|
|
123
|
+
if (allFindings.some((f) => severityRank(f.finding.severity) <= 1)) {
|
|
124
|
+
console.log(chalk.red('⚠ Critical/high findings detected. Review before publishing.'));
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"render.js","sourceRoot":"","sources":["../src/render.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAA;AAyBzB,kFAAkF;AAElF,MAAM,cAAc,GAA2B;IAC9C,QAAQ,EAAE,CAAC;IACX,IAAI,EAAE,CAAC;IACP,MAAM,EAAE,CAAC;IACT,GAAG,EAAE,CAAC;IACN,IAAI,EAAE,CAAC;CACP,CAAA;AAED,SAAS,YAAY,CAAC,CAAqB;IAC1C,OAAO,cAAc,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAA;AACpD,CAAC;AAED,SAAS,cAAc,CAAC,CAAqB;IAC5C,QAAQ,CAAC,EAAE,WAAW,EAAE,EAAE,CAAC;QAC1B,KAAK,UAAU;YACd,OAAO,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,WAAW,EAAE,GAAG,CAAC,CAAA;QACvD,KAAK,MAAM;YACV,OAAO,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAA;QACxC,KAAK,QAAQ;YACZ,OAAO,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAA;QAC3C,KAAK,KAAK;YACT,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC,CAAA;QACpC,KAAK,MAAM;YACV,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,EAAE,IAAI,MAAM,CAAC,CAAA;QAC7C;YACC,OAAO,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,SAAS,CAAC,CAAA;IAClC,CAAC;AACF,CAAC;AAED,kFAAkF;AAElF,MAAM,UAAU,YAAY,CAAC,IAA6B;IACzD,MAAM,MAAM,GAAG,IAAkB,CAAA;IACjC,MAAM,QAAQ,GAAG,MAAM,CAAC,QAAQ,IAAI,EAAE,CAAA;IAQtC,MAAM,WAAW,GAAkB,EAAE,CAAA;IACrC,IAAI,WAAW,GAAG,CAAC,CAAA;IACnB,IAAI,UAAU,GAAG,CAAC,CAAA;IAElB,KAAK,MAAM,CAAC,IAAI,QAAQ,EAAE,CAAC;QAC1B,WAAW,EAAE,CAAA;QACb,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACtD,UAAU,EAAE,CAAA;QACb,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,CAAC,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YAClC,WAAW,CAAC,IAAI,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC,OAAO,IAAI,SAAS,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAA;QAClE,CAAC;IACF,CAAC;IAED,mBAAmB;IACnB,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAA;IAE/F,SAAS;IACT,OAAO,CAAC,GAAG,EAAE,CAAA;IACb,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,wCAAwC,CAAC,CAAC,CAAA;IACxE,CAAC;SAAM,CAAC;QACP,MAAM,QAAQ,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAA;QACxF,MAAM,KAAK,GACV,QAAQ,GAAG,CAAC;YACX,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CACd,GAAG,WAAW,CAAC,MAAM,WAAW,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,KAAK,QAAQ,iBAAiB,CACjG;YACF,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,WAAW,CAAC,MAAM,WAAW,WAAW,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAA;QAC5F,OAAO,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC,IAAI,CAAC,aAAa,CAAC,MAAM,KAAK,EAAE,CAAC,CAAA;IACvD,CAAC;IAED,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,aAAa,UAAU,IAAI,WAAW,YAAY,CAAC,CAAC,CAAA;IAC1E,IAAI,MAAM,CAAC,KAAK,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,QAAQ,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAA;IAC/C,CAAC;IACD,OAAO,CAAC,GAAG,EAAE,CAAA;IAEb,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAM;IACP,CAAC;IAED,sDAAsD;IACtD,IAAI,YAAY,GAAG,EAAE,CAAA;IACrB,KAAK,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,IAAI,WAAW,EAAE,CAAC;QAChD,MAAM,GAAG,GAAG,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,SAAS,CAAA;QACxD,IAAI,GAAG,KAAK,YAAY,EAAE,CAAC;YAC1B,YAAY,GAAG,GAAG,CAAA;YAClB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,cAAc,CAAC,GAAG,CAAC,6BAA6B,CAAC,CAAC,CAAA;QAChF,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,WAAW,CAAC,CAAA;QAC7E,MAAM,IAAI,GAAG,OAAO,CAAC,WAAW,IAAI,EAAE,CAAA;QAEtC,OAAO,CAAC,GAAG,CAAC,KAAK,IAAI,KAAK,KAAK,CAAC,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC,CAAA;QACtD,IAAI,IAAI,EAAE,CAAC;YACV,+BAA+B;YAC/B,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;YAC7B,IAAI,IAAI,GAAG,IAAI,CAAA;YACf,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;gBAC1B,IAAI,IAAI,CAAC,MAAM,GAAG,IAAI,CAAC,MAAM,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC;oBACxC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;oBAC5B,IAAI,GAAG,KAAK,IAAI,EAAE,CAAA;gBACnB,CAAC;qBAAM,CAAC;oBACP,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,IAAI,CAAA;gBAC1C,CAAC;YACF,CAAC;YACD,IAAI,IAAI,CAAC,IAAI,EAAE,EAAE,CAAC;gBACjB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC,CAAA;YAC7B,CAAC;QACF,CAAC;QAED,qEAAqE;QACrE,MAAM,IAAI,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,aAAa,EAAE,UAAU,CAAC,CAAC,CAAA;QACzD,MAAM,MAAM,GAAG,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC;aACpC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;aAC7B,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,KAAK,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAA;QAC/C,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YACvB,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAA;QAC7D,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAA;IACd,CAAC;IAED,iCAAiC;IACjC,MAAM,cAAc,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,MAAM,KAAK,SAAS,CAAC,CAAA;IAC/F,IAAI,cAAc,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QAC/B,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,8CAA8C,CAAC,CAAC,CAAA;QACtE,KAAK,MAAM,CAAC,IAAI,cAAc,EAAE,CAAC;YAChC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,OAAO,IAAI,SAAS,KAAK,CAAC,CAAC,MAAM,IAAI,SAAS,EAAE,CAAC,CAAC,CAAA;QAChF,CAAC;QACD,OAAO,CAAC,GAAG,EAAE,CAAA;IACd,CAAC;IAED,YAAY;IACZ,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,YAAY,CAAC,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QACpE,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,GAAG,CAAC,+DAA+D,CAAC,CAAC,CAAA;IACxF,CAAC;AACF,CAAC"}
|
package/dist/scan.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core scan logic for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* CLI flow:
|
|
5
|
+
*
|
|
6
|
+
* load key → auto-update check → CI timeout pre-flight → POST scan → render
|
|
7
|
+
*
|
|
8
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
9
|
+
* │ scan.ts │
|
|
10
|
+
* │ │
|
|
11
|
+
* │ 1. Pre-flight: GET /api/v1/healthz?delay=65 (70s limit) │
|
|
12
|
+
* │ → detects CI outbound timeout < 240s │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ 2. POST /api/v1/scan (multipart) │
|
|
15
|
+
* │ fields: artifact (file), apiKey, extensionId? │
|
|
16
|
+
* │ timeout: 240s │
|
|
17
|
+
* │ │
|
|
18
|
+
* │ 3. Render: pretty (chalk) or raw JSON (--json) │
|
|
19
|
+
* └──────────────────────────────────────────────────────────┘
|
|
20
|
+
*
|
|
21
|
+
* No automatic retry on network failure — the plan specifies:
|
|
22
|
+
* "при network error не делайте автоматический retry —
|
|
23
|
+
* списанный credit вернётся автоматически в течение 1 минуты (auto-refund cron)"
|
|
24
|
+
*/
|
|
25
|
+
export interface ScanOptions {
|
|
26
|
+
artifactPath: string;
|
|
27
|
+
apiKey: string;
|
|
28
|
+
apiUrl: string;
|
|
29
|
+
extensionId?: string;
|
|
30
|
+
outputJson: boolean;
|
|
31
|
+
skipPreflight: boolean;
|
|
32
|
+
}
|
|
33
|
+
export declare function runScan(opts: ScanOptions): Promise<void>;
|
|
34
|
+
//# sourceMappingURL=scan.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAUH,MAAM,WAAW,WAAW;IAC3B,YAAY,EAAE,MAAM,CAAA;IACpB,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,MAAM,CAAA;IACd,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,UAAU,EAAE,OAAO,CAAA;IACnB,aAAa,EAAE,OAAO,CAAA;CACtB;AAED,wBAAsB,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAoF9D"}
|
package/dist/scan.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core scan logic for the Extenshi CLI.
|
|
3
|
+
*
|
|
4
|
+
* CLI flow:
|
|
5
|
+
*
|
|
6
|
+
* load key → auto-update check → CI timeout pre-flight → POST scan → render
|
|
7
|
+
*
|
|
8
|
+
* ┌──────────────────────────────────────────────────────────┐
|
|
9
|
+
* │ scan.ts │
|
|
10
|
+
* │ │
|
|
11
|
+
* │ 1. Pre-flight: GET /api/v1/healthz?delay=65 (70s limit) │
|
|
12
|
+
* │ → detects CI outbound timeout < 240s │
|
|
13
|
+
* │ │
|
|
14
|
+
* │ 2. POST /api/v1/scan (multipart) │
|
|
15
|
+
* │ fields: artifact (file), apiKey, extensionId? │
|
|
16
|
+
* │ timeout: 240s │
|
|
17
|
+
* │ │
|
|
18
|
+
* │ 3. Render: pretty (chalk) or raw JSON (--json) │
|
|
19
|
+
* └──────────────────────────────────────────────────────────┘
|
|
20
|
+
*
|
|
21
|
+
* No automatic retry on network failure — the plan specifies:
|
|
22
|
+
* "при network error не делайте автоматический retry —
|
|
23
|
+
* списанный credit вернётся автоматически в течение 1 минуты (auto-refund cron)"
|
|
24
|
+
*/
|
|
25
|
+
import fs from 'node:fs';
|
|
26
|
+
import path from 'node:path';
|
|
27
|
+
import { FormData, fetch } from 'undici';
|
|
28
|
+
import { renderReport } from './render.js';
|
|
29
|
+
const SCAN_TIMEOUT_MS = 240_000; // 240 seconds
|
|
30
|
+
const PREFLIGHT_TIMEOUT_MS = 70_000; // 70 seconds
|
|
31
|
+
export async function runScan(opts) {
|
|
32
|
+
const { artifactPath, apiKey, apiUrl, extensionId, outputJson, skipPreflight } = opts;
|
|
33
|
+
// Validate artifact file exists
|
|
34
|
+
if (!fs.existsSync(artifactPath)) {
|
|
35
|
+
throw new Error(`Artifact file not found: ${artifactPath}`);
|
|
36
|
+
}
|
|
37
|
+
const artifactStats = fs.statSync(artifactPath);
|
|
38
|
+
if (!artifactStats.isFile()) {
|
|
39
|
+
throw new Error(`Artifact path is not a file: ${artifactPath}`);
|
|
40
|
+
}
|
|
41
|
+
const MAX_SIZE = 50 * 1024 * 1024;
|
|
42
|
+
if (artifactStats.size > MAX_SIZE) {
|
|
43
|
+
throw new Error(`Artifact file exceeds 50 MB limit (${(artifactStats.size / 1024 / 1024).toFixed(1)} MB)`);
|
|
44
|
+
}
|
|
45
|
+
// Step 1: CI timeout pre-flight
|
|
46
|
+
if (!skipPreflight) {
|
|
47
|
+
await runPreflight(apiUrl);
|
|
48
|
+
}
|
|
49
|
+
// Step 2: Upload and scan
|
|
50
|
+
process.stderr.write('Scanning...\n');
|
|
51
|
+
const artifactData = fs.readFileSync(artifactPath);
|
|
52
|
+
const artifactName = path.basename(artifactPath);
|
|
53
|
+
const form = new FormData();
|
|
54
|
+
form.append('apiKey', apiKey);
|
|
55
|
+
form.append('artifact', new File([artifactData], artifactName));
|
|
56
|
+
if (extensionId) {
|
|
57
|
+
form.append('extensionId', extensionId);
|
|
58
|
+
}
|
|
59
|
+
let response;
|
|
60
|
+
try {
|
|
61
|
+
response = await fetch(`${apiUrl}/api/v1/scan`, {
|
|
62
|
+
method: 'POST',
|
|
63
|
+
body: form,
|
|
64
|
+
signal: AbortSignal.timeout(SCAN_TIMEOUT_MS),
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
69
|
+
throw new Error(`Scan request timed out after ${SCAN_TIMEOUT_MS / 1000}s.\n` +
|
|
70
|
+
'This may indicate a network issue or a slow scan.\n' +
|
|
71
|
+
'If a credit was charged, it will be auto-refunded within 1 minute.\n' +
|
|
72
|
+
'Do NOT retry immediately — wait for the auto-refund before trying again.');
|
|
73
|
+
}
|
|
74
|
+
throw err;
|
|
75
|
+
}
|
|
76
|
+
const body = (await response.json());
|
|
77
|
+
if (!response.ok) {
|
|
78
|
+
const errorMsg = typeof body.error === 'string' ? body.error : `HTTP ${response.status}`;
|
|
79
|
+
// Map known status codes to actionable messages
|
|
80
|
+
if (response.status === 401) {
|
|
81
|
+
throw new Error(`Authentication failed: ${errorMsg}\nCheck your EXTENSHI_API_KEY.`);
|
|
82
|
+
}
|
|
83
|
+
if (response.status === 402) {
|
|
84
|
+
throw new Error(`Insufficient credits.\nPurchase more at https://dojo.extenshi.io/billing/cli`);
|
|
85
|
+
}
|
|
86
|
+
if (response.status === 403) {
|
|
87
|
+
throw new Error(`Access denied: ${errorMsg}`);
|
|
88
|
+
}
|
|
89
|
+
if (response.status === 429) {
|
|
90
|
+
const retryAfter = body.retryAfterSec ?? 60;
|
|
91
|
+
throw new Error(`Rate limit exceeded. Please wait ${retryAfter}s before trying again.`);
|
|
92
|
+
}
|
|
93
|
+
throw new Error(`Scan failed: ${errorMsg}`);
|
|
94
|
+
}
|
|
95
|
+
// Step 3: Output
|
|
96
|
+
if (outputJson) {
|
|
97
|
+
process.stdout.write(`${JSON.stringify(body, null, 2)}\n`);
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
renderReport(body);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* CI timeout pre-flight probe.
|
|
105
|
+
*
|
|
106
|
+
* Sends GET /api/v1/healthz?delay=65 — the server sleeps 65s then responds 200.
|
|
107
|
+
* If we don't get a response within 70s, the CI runner's outbound timeout is
|
|
108
|
+
* shorter than the 240s the scan needs.
|
|
109
|
+
*/
|
|
110
|
+
async function runPreflight(apiUrl) {
|
|
111
|
+
try {
|
|
112
|
+
await fetch(`${apiUrl}/api/v1/healthz?delay=65`, {
|
|
113
|
+
signal: AbortSignal.timeout(PREFLIGHT_TIMEOUT_MS),
|
|
114
|
+
});
|
|
115
|
+
// Got response → timeout is at least 70s, proceed
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
if (err instanceof Error && err.name === 'TimeoutError') {
|
|
119
|
+
throw new Error(`Your CI/network outbound HTTP timeout appears shorter than the 240s extenshi scan needs.\n\n` +
|
|
120
|
+
`The pre-flight probe (65s delay) did not complete within 70s.\n\n` +
|
|
121
|
+
`To fix:\n` +
|
|
122
|
+
` • GitHub Actions: add timeout-minutes: 10 to the scan step\n` +
|
|
123
|
+
` • Corporate proxy: contact IT to increase the outbound timeout\n` +
|
|
124
|
+
` • To skip this check (at your own risk): use --skip-preflight\n\n` +
|
|
125
|
+
`No credit was charged — the scan was not attempted.`);
|
|
126
|
+
}
|
|
127
|
+
// Network errors (not timeout) — warn and proceed
|
|
128
|
+
process.stderr.write(`Pre-flight probe failed (${err.message}) — proceeding anyway.\n`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scan.js","sourceRoot":"","sources":["../src/scan.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@extenshi/cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Pre-publish security scanner for browser extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"bin": {
|
|
10
|
+
"extenshi": "dist/cli.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"dev": "tsc --watch",
|
|
18
|
+
"test": "vitest run",
|
|
19
|
+
"prepublishOnly": "yarn build"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"chalk": "^5.3.0",
|
|
23
|
+
"commander": "^12.1.0",
|
|
24
|
+
"ora": "^8.1.1",
|
|
25
|
+
"semver": "^7.6.3",
|
|
26
|
+
"undici": "^7.3.0"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@types/node": "^22.16.5",
|
|
30
|
+
"@types/semver": "^7.5.8",
|
|
31
|
+
"typescript": "^5.8.3",
|
|
32
|
+
"vitest": "^3.2.4"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
}
|
|
37
|
+
}
|