correctover-scan 3.1.1 → 3.2.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.
@@ -195,7 +195,11 @@ const METADATA_HOSTS = /metadata\.google(?:\.internal)?|169\.254\.|fd00:ec2/i;
195
195
 
196
196
  // Context that proves a 169.254/private address is a cloud SDK credential
197
197
  // provider (normal) rather than business-code SSRF (finding).
198
- const SDK_CTX = /(aws|amazon|ec2|ecs|eks|imds|metadata\s*(service|endpoint|host)|credentialprovider|container\s*credentials|ecr|sts\.)/i;
198
+ // Strong signals only: the metadata address must actually sit inside a cloud
199
+ // SDK credential-provider call. A bare AWS_/GCP env-var name nearby is NOT
200
+ // proof of SDK usage — it often co-occurs with a hand-rolled SSRF and used to
201
+ // wrongly suppress it. Keep this narrow to avoid false negatives.
202
+ const SDK_CTX = /(aws-sdk|@aws-sdk|amazon-cognito|aws4fetch|fromNodeProviderChain|fromIni|ECSCredentials|EC2MetadataClient|instanceMetadataService|metadataService\b|[A-Za-z_$][\w$]*[Mm]etadata\s*[=(]|metadata\s*(?:service|endpoint)\s*[:=]|credentialprovider|credential[ _-]?provider|credentials?\s*(?:chain|from)|container\s*credentials|sts\.|gcp-metadata|google-auth-library)/i;
199
203
  // Context that proves the address sits inside an SSRF guard / blocklist.
200
204
  // Actual guard constructs (a bare "ssrf" word in a comment is not a guard).
201
205
  const GUARD_CTX = /(no_?proxy|blocklist|block_list|denylist|deny_list|allowlist|preflight|isPrivate|private[_-]?ip|loopback|metadata.{0,30}(block|deny|filter|redirect))/i;
@@ -391,7 +395,9 @@ function checkMetadataIntranet(file) {
391
395
  seen.add(target + ':' + i);
392
396
  const ctx = lineWindow(file.lines, i);
393
397
  const isMetadata = /^169\.254\./.test(target) || /metadata\.google/i.test(target) || /fd00:ec2/i.test(target);
394
- const inSdk = SDK_CTX.test(ctx) || /SECONDARY_HOST_ADDRESS|gcp|google/i.test(ctx);
398
+ // Bare "gcp"/"google" words are too weak and suppress real SSRFs; require
399
+ // concrete SDK/host markers instead (SDK_CTX already covers the strong ones).
400
+ const inSdk = SDK_CTX.test(ctx) || /SECONDARY_HOST_ADDRESS|gcp-metadata|metadata\.google(?:\.internal)?/.test(ctx);
395
401
  const inGuard = GUARD_CTX.test(ctx);
396
402
  let sev, msg, suppressed = false;
397
403
  if (inSdk) {
@@ -417,7 +423,7 @@ function checkMetadataIntranet(file) {
417
423
  function checkCommandExec(file) {
418
424
  const findings = [];
419
425
  const shellTrue = /shell\s*:\s*(?:!0|true)/;
420
- const execCall = /(^|[^.\w$])(?:exec|execSync)\s*\(/;
426
+ const execCall = /(?<![\w$])(?:exec|execSync)\s*\(/;
421
427
  const gatedRe = /(permission[A-Z]|permission[ _-]?(?:check|mode|behavior|behaviour|prompt|decision|system|gate)|allowedTools|disallowedTools|shell-quote|shellQuote|\.quote\(|PreToolUse|PostToolUse|hookEventName|"Bash"|Bash tool|isCommandAllowed|canUseBash|checkPermissions|permissionRule|toolPermission)/i;
422
428
  const buildRe = /(node-gyp|npm install|install script|spawnSync\(\s*["'`])(?:[a-z\-]+ )?[a-z\-]+(?:\s|["'`])/i;
423
429
  // call site where the command arg is a fixed literal ("cmd ...")
@@ -456,9 +462,14 @@ function checkCommandExec(file) {
456
462
  if (seen.has('exec:' + i)) return;
457
463
  seen.add('exec:' + i);
458
464
  const ctx = lineWindow(file.lines, i);
459
- const looksDynamic = dynamicCmd.test(line) || /\$\{/.test(ctx);
465
+ // Variable passed straight through as the command arg, e.g. exec(userInput)
466
+ // or execSync(cmd, opts) — no template/concat syntax but still fully
467
+ // attacker-controllable. String literals are excluded by the identifier
468
+ // pattern; method chains like obj.execute(x) are not matched (execCall).
469
+ const varArg = /(?:exec|execSync)\s*\(\s*[A-Za-z_$][\w$]*\s*[,\)]/.test(line);
470
+ const looksDynamic = dynamicCmd.test(line) || /\$\{/.test(ctx) || varArg;
460
471
  findings.push(mkFinding(file, i + 1, looksDynamic ? 'warn' : 'info',
461
- `child_process exec/execSync 调用(始终经 shell)${looksDynamic ? ',参数疑似动态拼接 — 检查注入面' : ',参数为静态/库内字符串'}`,
472
+ `child_process exec/execSync 调用(始终经 shell)${looksDynamic ? ',命令参数疑似动态/外部输入 — 检查命令注入面' : ',参数为静态/库内字符串'}`,
462
473
  { suppressed: !looksDynamic, snippet: snippet(line), checkId: 'command-exec' }));
463
474
  }
464
475
  });
package/core/skillpay.js CHANGED
@@ -2,8 +2,10 @@
2
2
  * SkillPay (Alipay) acquisition bridge for users in mainland China.
3
3
  *
4
4
  * One public shelf URL covers every purchasable product; single-product deep
5
- * links, if later provided, take precedence automatically. No prices are
6
- * printed anywhere here — only a pointer to the shelf.
5
+ * links, if later provided, take precedence automatically.
6
+ *
7
+ * Prices ARE printed by checkoutBlock() — explicitly authorized 2026-09-27 to
8
+ * close the scan→payment gap. Keep them in sync with the live Alipay shelf.
7
9
  */
8
10
 
9
11
  'use strict';
@@ -18,6 +20,12 @@ const PRODUCTS = {
18
20
  audit: { name: 'CCS收据批量审计', url: '' },
19
21
  };
20
22
 
23
+ // Shelf prices (CNY). Owner-supplied 2026-09-27; keep identical to the shelf.
24
+ const OFFERS = {
25
+ cert: { label: 'CCS 运行时认证 · 正式合规凭证', price: '¥699/年' },
26
+ checkup: { label: 'MCP 安全体检 · 持续监控 + 月报', price: '¥49.9/月' },
27
+ };
28
+
21
29
  function isChinaUser() {
22
30
  if (process.env.CORRECTOVER_REGION === 'CN') return true;
23
31
  if (process.env.CORRECTOVER_REGION === 'GLOBAL') return false;
@@ -36,6 +44,30 @@ function shelfUrlFor(key) {
36
44
  return (p && p.url) || STORE_URL;
37
45
  }
38
46
 
47
+ /**
48
+ * Scan→payment checkout block. Personalized by the number of risks found so the
49
+ * user moves from "you're at risk" straight to a payable next step.
50
+ *
51
+ * @param {number} riskCount fail-level findings in this scan
52
+ * @returns {string} multi-line block, or '' for non-China users
53
+ */
54
+ function checkoutBlock(riskCount) {
55
+ if (!isChinaUser()) return '';
56
+ const n = Number.isFinite(riskCount) ? riskCount : 0;
57
+ const url = shelfUrlFor('scan');
58
+ const out = [];
59
+ out.push('🇨🇳 支付宝 SkillPay · 检出风险后直接处理');
60
+ if (n > 0) {
61
+ out.push(` 本次有 ${n} 项风险提示。别让它停在「你完蛋了」——`);
62
+ } else {
63
+ out.push(' 本次未发现风险。趁现在拿到正式凭证——');
64
+ }
65
+ out.push(` · ${OFFERS.cert.label} ${OFFERS.cert.price}`);
66
+ out.push(` · ${OFFERS.checkup.label} ${OFFERS.checkup.price}`);
67
+ out.push(` 打开即扫码付款:${url}`);
68
+ return out.join('\n');
69
+ }
70
+
39
71
  /**
40
72
  * Full China acquisition block. Returns '' for non-China users so behaviour is
41
73
  * identical to pre-bridge releases unless a CN user is detected.
@@ -66,7 +98,9 @@ function ctaLine(productKey) {
66
98
  module.exports = {
67
99
  STORE_URL,
68
100
  PRODUCTS,
101
+ OFFERS,
69
102
  isChinaUser,
103
+ checkoutBlock,
70
104
  block,
71
105
  ctaLine,
72
106
  };
package/index.js CHANGED
@@ -15,7 +15,7 @@ const { recordCall, getUpgradeMessage } = require('./core/license');
15
15
  const skillpay = require('./core/skillpay');
16
16
  const { formatHTMLReport, normalizeConfigFindings, normalizeBundleFindings, failHint } = require('./core/report');
17
17
 
18
- const VERSION = '3.1.1';
18
+ const VERSION = '3.2.0';
19
19
  const PRODUCT = 'correctover-scan';
20
20
  const JS_EXT = new Set(['.js', '.mjs', '.cjs', '.ts']);
21
21
 
@@ -133,7 +133,7 @@ function formatResults(results, stats, filename) {
133
133
  lines.push('');
134
134
  lines.push(`${c.dim}Free automated scan covers surface-level checks. A manual Correctover audit goes deeper: 116 semantic intent rules, 5-day turnaround, findings grounded in real MCP ecosystem CVEs. First customers: if we find no critical-severity issue, you pay nothing. \u2192 ${c.cyan}https://dshcorrectover.github.io/agent-audit/${c.reset}`);
135
135
 
136
- const cnBlock = skillpay.block('scan');
136
+ const cnBlock = skillpay.checkoutBlock(stats.fail + stats.warn);
137
137
  if (cnBlock) {
138
138
  lines.push('');
139
139
  lines.push(cnBlock);
@@ -212,7 +212,7 @@ function formatBundleResults(bundle, baseLabel) {
212
212
  lines.push('');
213
213
  lines.push(`${c.dim}Web: https://correctover.com/scan/ | GitHub: https://github.com/DSHCorrectover${c.reset}`);
214
214
 
215
- const cnBundleBlock = skillpay.block('scan');
215
+ const cnBundleBlock = skillpay.checkoutBlock(activeRiskCount(bundle));
216
216
  if (cnBundleBlock) {
217
217
  lines.push('');
218
218
  lines.push(cnBundleBlock);
@@ -220,6 +220,21 @@ function formatBundleResults(bundle, baseLabel) {
220
220
  return lines.join('\n');
221
221
  }
222
222
 
223
+ function activeRiskCount(bundle) {
224
+ // Every unsuppressed warn/fail finding is something the user should act on,
225
+ // not just confirmed fails (the bundle engine suppresses known-benign
226
+ // signals and can under-classify simple variable args as static).
227
+ let n = 0;
228
+ for (const fr of bundle.files) {
229
+ for (const r of fr.results) {
230
+ for (const f of r.findings) {
231
+ if (!f.suppressed && (f.severity === 'fail' || f.severity === 'warn')) n++;
232
+ }
233
+ }
234
+ }
235
+ return n;
236
+ }
237
+
223
238
  function formatBundleJSON(bundle, target) {
224
239
  return JSON.stringify({
225
240
  scanner: 'correctover-scan', version: VERSION, mode: 'bundle', target,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "correctover-scan",
3
- "version": "3.1.1",
3
+ "version": "3.2.1",
4
4
  "description": "Correctover Security Scanner — CCS audit for MCP configurations AND published JS bundles/npm packages. Detects hardcoded secrets, RCE, SSRF, credential hijacking against OWASP AISVS 1.0.",
5
5
  "main": "index.js",
6
6
  "bin": {