canship 0.8.0 → 0.8.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/README-zh-CN.md CHANGED
@@ -6,20 +6,20 @@
6
6
 
7
7
  [English](./README.md) · [使用参考](./docs/reference-zh-CN.md) · [发布说明](https://github.com/Tasomei/canship/releases) · [npm](https://www.npmjs.com/package/canship)
8
8
 
9
- > 本文对应 npm `latest` 渠道的 `0.8.0`。其他版本见[发布说明](https://github.com/Tasomei/canship/releases)。
9
+ > 本文对应 npm `latest` 渠道的 `0.8.1`。其他版本见[发布说明](https://github.com/Tasomei/canship/releases)。
10
10
 
11
11
  ## 快速开始
12
12
 
13
13
  要求 Node.js 18 及以上,无运行时依赖。
14
14
 
15
15
  ```powershell
16
- npx canship@0.8.0
16
+ npx canship@0.8.1
17
17
  ```
18
18
 
19
19
  扫描指定目录:
20
20
 
21
21
  ```powershell
22
- npx canship@0.8.0 "./my-app"
22
+ npx canship@0.8.1 "./my-app"
23
23
  ```
24
24
 
25
25
  合成项目的输出示例:
@@ -39,7 +39,7 @@ npx canship@0.8.0 "./my-app"
39
39
  路由分析覆盖 Next.js、SvelteKit、Nuxt、Remix / React Router、Astro、Express、Hono、Fastify 的指定入口,详见[入口及限制](./docs/reference-zh-CN.md#服务端入口)。
40
40
 
41
41
  ```powershell
42
- npx canship@0.8.0 --list-rules
42
+ npx canship@0.8.1 --list-rules
43
43
  ```
44
44
 
45
45
  ## 审阅结果
@@ -47,11 +47,11 @@ npx canship@0.8.0 --list-rules
47
47
  每条结果标为 `certain`(静态证据充分)或 `likely`(需人工审阅),默认只显示 `certain`。两者都不代表凭据有效或问题可被利用。
48
48
 
49
49
  ```powershell
50
- npx canship@0.8.0 --all --verbose
50
+ npx canship@0.8.1 --all --verbose
51
51
  ```
52
52
 
53
53
  ```powershell
54
- npx canship@0.8.0 --all --report
54
+ npx canship@0.8.1 --all --report
55
55
  ```
56
56
 
57
57
  ![HTML 报告](https://raw.githubusercontent.com/Tasomei/canship/main/docs/images/report.png)
@@ -82,9 +82,9 @@ jobs:
82
82
  with:
83
83
  fetch-depth: 0
84
84
  persist-credentials: false
85
- - uses: Tasomei/canship@8ae4d5f4508fbd68fc2cf440e138c1217064a0e0
85
+ - uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
86
86
  with:
87
- version: '0.8.0'
87
+ version: '0.8.1'
88
88
  honor-ignore-markers: false
89
89
  ```
90
90
 
@@ -92,7 +92,7 @@ jobs:
92
92
 
93
93
  | 输入 | 固定 Action 的默认值 | 含义 |
94
94
  |---|---|---|
95
- | `version` | `0.8.0` | 精确的 npm 扫描器版本;请如上例显式设置 |
95
+ | `version` | `0.8.1` | 精确的 npm 扫描器版本;请如上例显式设置 |
96
96
  | `fail-on` | `blocking` | `blocking`:`certain` 的 P0/P1;`any`:全部结果;`none`:仅报告 |
97
97
 
98
98
  完整说明见 [Action 输入](https://github.com/Tasomei/canship/blob/main/action.yml)。
package/README.md CHANGED
@@ -6,20 +6,20 @@ Static scans run locally: they are read-only, execute no project code and make n
6
6
 
7
7
  [简体中文](./README-zh-CN.md) · [Reference](./docs/reference.md) · [Releases](https://github.com/Tasomei/canship/releases) · [npm](https://www.npmjs.com/package/canship)
8
8
 
9
- > Documentation for `0.8.0` on npm `latest`. Other versions are listed under [Releases](https://github.com/Tasomei/canship/releases).
9
+ > Documentation for `0.8.1` on npm `latest`. Other versions are listed under [Releases](https://github.com/Tasomei/canship/releases).
10
10
 
11
11
  ## Quick start
12
12
 
13
13
  Requires Node.js 18 or later; no runtime dependencies.
14
14
 
15
15
  ```powershell
16
- npx canship@0.8.0
16
+ npx canship@0.8.1
17
17
  ```
18
18
 
19
19
  Scan a specific directory:
20
20
 
21
21
  ```powershell
22
- npx canship@0.8.0 "./my-app"
22
+ npx canship@0.8.1 "./my-app"
23
23
  ```
24
24
 
25
25
  Example output for a synthetic project:
@@ -39,7 +39,7 @@ Example output for a synthetic project:
39
39
  Route analysis covers documented entry points in Next.js, SvelteKit, Nuxt, Remix / React Router, Astro, Express, Hono and Fastify. See [entry points and limits](./docs/reference.md#server-entry-points).
40
40
 
41
41
  ```powershell
42
- npx canship@0.8.0 --list-rules
42
+ npx canship@0.8.1 --list-rules
43
43
  ```
44
44
 
45
45
  ## Reviewing findings
@@ -47,11 +47,11 @@ npx canship@0.8.0 --list-rules
47
47
  Each finding is rated `certain` (strong static evidence) or `likely` (needs review); only `certain` findings are shown by default. Neither rating proves that a credential is valid or that an issue is exploitable.
48
48
 
49
49
  ```powershell
50
- npx canship@0.8.0 --all --verbose
50
+ npx canship@0.8.1 --all --verbose
51
51
  ```
52
52
 
53
53
  ```powershell
54
- npx canship@0.8.0 --all --report
54
+ npx canship@0.8.1 --all --report
55
55
  ```
56
56
 
57
57
  ![HTML report](https://raw.githubusercontent.com/Tasomei/canship/main/docs/images/report.png)
@@ -82,9 +82,9 @@ jobs:
82
82
  with:
83
83
  fetch-depth: 0
84
84
  persist-credentials: false
85
- - uses: Tasomei/canship@8ae4d5f4508fbd68fc2cf440e138c1217064a0e0
85
+ - uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
86
86
  with:
87
- version: '0.8.0'
87
+ version: '0.8.1'
88
88
  honor-ignore-markers: false
89
89
  ```
90
90
 
@@ -92,7 +92,7 @@ The commit hash pins the Action and `version` pins the npm scanner. The Action d
92
92
 
93
93
  | Input | Default in the pinned Action | Meaning |
94
94
  |---|---|---|
95
- | `version` | `0.8.0` | Exact npm scanner version; set explicitly as above |
95
+ | `version` | `0.8.1` | Exact npm scanner version; set explicitly as above |
96
96
  | `fail-on` | `blocking` | `blocking`: `certain` P0/P1; `any`: all findings; `none`: report only |
97
97
 
98
98
  See all [Action inputs](https://github.com/Tasomei/canship/blob/main/action.yml).
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // <define:__CANSHIP_BUILD_INFO__>
4
- var define_CANSHIP_BUILD_INFO_default = { version: "0.8.0", channel: "release", revision: "0700a3d119573b1d13c5f5b60f4e93ab87244b35", dirty: false };
4
+ var define_CANSHIP_BUILD_INFO_default = { version: "0.8.1", channel: "release", revision: "25d115c41e1a9a9455d19f448fc2265d87843b74", dirty: false };
5
5
 
6
6
  // src/cli.ts
7
7
  import { resolve as resolve7 } from "path";
@@ -8138,8 +8138,8 @@ var InputFlow = class {
8138
8138
  if (expr.length > MAX_EXPRESSION) this.limited = true;
8139
8139
  const text = expr.trim();
8140
8140
  if (text === "" || CLEAN_EXPRESSION.some((pattern) => pattern.test(text)) || isComparison(text)) return null;
8141
- const lookup = new RegExp(`^([A-Za-z_$][\\w$]*)\\s*\\[[^\\]]*\\]\\s*(?:(?:\\?\\?|\\|\\|)\\s*${LITERAL})?\\s*$`).exec(text);
8142
- if (lookup && this.isFixedTable(lookup[1], exprAt)) return null;
8141
+ const lookup2 = new RegExp(`^([A-Za-z_$][\\w$]*)\\s*\\[[^\\]]*\\]\\s*(?:(?:\\?\\?|\\|\\|)\\s*${LITERAL})?\\s*$`).exec(text);
8142
+ if (lookup2 && this.isFixedTable(lookup2[1], exprAt)) return null;
8143
8143
  const observed = expr.replace(/\[\s*['"][^'"]*['"]\s*\]/g, (part, offset) => {
8144
8144
  const raw = this.source.slice(exprAt + offset, exprAt + offset + part.length);
8145
8145
  const key = /^\[\s*['"]([A-Za-z_$][\w$]*)['"]\s*\]$/.exec(raw)?.[1];
@@ -11736,7 +11736,7 @@ function openReport(path, onError, options = {}) {
11736
11736
  }
11737
11737
 
11738
11738
  // src/build-info.ts
11739
- var VERSION = true ? "0.8.0" : "0.0.0-dev";
11739
+ var VERSION = true ? "0.8.1" : "0.0.0-dev";
11740
11740
  var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
11741
11741
  function getBuildInfo() {
11742
11742
  return { ...identity };
@@ -12072,7 +12072,7 @@ ${matrix} runs-on: ubuntu-latest
12072
12072
  with:
12073
12073
  fetch-depth: 0
12074
12074
  persist-credentials: false
12075
- - uses: Tasomei/canship@8ae4d5f4508fbd68fc2cf440e138c1217064a0e0
12075
+ - uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
12076
12076
  with:
12077
12077
  version: '${version}'
12078
12078
  ${target} honor-ignore-markers: false
@@ -12761,7 +12761,7 @@ function createProbePlan(input, expectAuth = false, canarySha256) {
12761
12761
  }
12762
12762
 
12763
12763
  // src/probe-network.ts
12764
- import { Resolver } from "dns/promises";
12764
+ import { Resolver, lookup } from "dns/promises";
12765
12765
  import { isIP as isIP2 } from "net";
12766
12766
  import { Agent, request } from "https";
12767
12767
  import { createHash as createHash7 } from "crypto";
@@ -12778,10 +12778,38 @@ function checkProbeRuntime(env = process.env, args = process.execArgv) {
12778
12778
  throw new ProbeError("PROBE_RUNTIME_UNSUPPORTED", "Network tracing or TLS overrides are active. No probe was sent; use static scanning or an approved standard runtime.");
12779
12779
  }
12780
12780
  }
12781
- async function resolveProbeAddresses(hostname, signal, createResolver = () => new Resolver({ timeout: PROBE_LIMITS.dnsTimeoutMs, tries: 1 })) {
12781
+ var DNS_FAILED = () => new ProbeError("PROBE_DNS_FAILED", "DNS resolution did not complete for the requested target.");
12782
+ var DNS_TIMEOUT = () => new ProbeError("PROBE_DNS_TIMEOUT", "DNS resolution exceeded the probe time limit.");
12783
+ var DNS_UNREACHABLE = ["ECONNREFUSED", "ETIMEOUT"];
12784
+ async function systemAddresses(hostname, signal, remainingMs, systemLookup) {
12785
+ if (remainingMs <= 0) throw DNS_TIMEOUT();
12786
+ let timer;
12787
+ let onAbort;
12788
+ try {
12789
+ const entries = await Promise.race([
12790
+ systemLookup(hostname).catch(() => {
12791
+ throw DNS_FAILED();
12792
+ }),
12793
+ new Promise((_resolve, reject) => {
12794
+ timer = setTimeout(() => reject(DNS_TIMEOUT()), remainingMs);
12795
+ }),
12796
+ new Promise((_resolve, reject) => {
12797
+ onAbort = () => reject(new ProbeError("PROBE_CANCELLED", "Probe cancelled; no complete validation result was produced."));
12798
+ signal?.addEventListener("abort", onAbort, { once: true });
12799
+ })
12800
+ ]);
12801
+ if (!Array.isArray(entries)) throw DNS_FAILED();
12802
+ return entries.filter((entry) => entry && (entry.family === 4 || entry.family === 6) && typeof entry.address === "string").map((entry) => ({ address: entry.address, family: entry.family }));
12803
+ } finally {
12804
+ clearTimeout(timer);
12805
+ if (onAbort) signal?.removeEventListener("abort", onAbort);
12806
+ }
12807
+ }
12808
+ async function resolveProbeAddresses(hostname, signal, createResolver = () => new Resolver({ timeout: PROBE_LIMITS.dnsTimeoutMs, tries: 1 }), systemLookup = (name) => lookup(name, { all: true, verbatim: true })) {
12782
12809
  checkProbeCancelled(signal);
12783
12810
  const literal2 = isIP2(hostname);
12784
12811
  if (literal2 === 4 || literal2 === 6) return [{ address: hostname, family: literal2 }];
12812
+ const started = Date.now();
12785
12813
  const resolver = createResolver();
12786
12814
  let timedOut = false, stopped = false;
12787
12815
  const cancel = () => {
@@ -12807,11 +12835,14 @@ async function resolveProbeAddresses(hostname, signal, createResolver = () => ne
12807
12835
  })
12808
12836
  ]);
12809
12837
  checkProbeCancelled(signal);
12810
- if (timedOut) throw new ProbeError("PROBE_DNS_TIMEOUT", "DNS resolution exceeded the probe time limit.");
12838
+ if (timedOut) throw DNS_TIMEOUT();
12839
+ if (a.status === "rejected" && aaaa.status === "rejected" && [a.reason?.code, aaaa.reason?.code].every((code) => DNS_UNREACHABLE.includes(String(code)))) {
12840
+ return await systemAddresses(hostname, signal, PROBE_LIMITS.dnsTimeoutMs - (Date.now() - started), systemLookup);
12841
+ }
12811
12842
  const addresses = [];
12812
12843
  for (const [result, family] of [[a, 4], [aaaa, 6]]) {
12813
12844
  if (result.status === "fulfilled") addresses.push(...result.value.map((address) => ({ address, family })));
12814
- else if (!["ENODATA", "ENOTFOUND"].includes(String(result.reason?.code))) throw new ProbeError("PROBE_DNS_FAILED", "DNS resolution did not complete for the requested target.");
12845
+ else if (!["ENODATA", "ENOTFOUND"].includes(String(result.reason?.code))) throw DNS_FAILED();
12815
12846
  }
12816
12847
  return addresses;
12817
12848
  } finally {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // <define:__CANSHIP_BUILD_INFO__>
4
- var define_CANSHIP_BUILD_INFO_default = { version: "0.8.0", channel: "release", revision: "0700a3d119573b1d13c5f5b60f4e93ab87244b35", dirty: false };
4
+ var define_CANSHIP_BUILD_INFO_default = { version: "0.8.1", channel: "release", revision: "25d115c41e1a9a9455d19f448fc2265d87843b74", dirty: false };
5
5
 
6
6
  // src/index.ts
7
7
  import { statSync as statSync3 } from "fs";
@@ -9987,7 +9987,7 @@ function summarize(result) {
9987
9987
  }
9988
9988
 
9989
9989
  // src/build-info.ts
9990
- var VERSION = true ? "0.8.0" : "0.0.0-dev";
9990
+ var VERSION = true ? "0.8.1" : "0.0.0-dev";
9991
9991
  var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
9992
9992
  function getBuildInfo() {
9993
9993
  return { ...identity };
@@ -2,7 +2,7 @@
2
2
 
3
3
  [概览](../README-zh-CN.md) · [English](./reference.md)
4
4
 
5
- 本文对应 `0.8.0`。其他版本见[发布说明](https://github.com/Tasomei/canship/releases)。
5
+ 本文对应 `0.8.1`。其他版本见[发布说明](https://github.com/Tasomei/canship/releases)。
6
6
 
7
7
  ## 服务端入口
8
8
 
@@ -129,11 +129,11 @@ OpenAPI 配置支持内联对象、常量及静态 ESM 导入与重导出,路
129
129
 
130
130
  `--probe=https://app.example.com/status` 预览 HEAD 和携带固定测试 Origin 的 OPTIONS 请求。使用前替换为获授权的目标。审阅目标、限制及隐私提示后,保留原选项并附加展示的 `--confirm-probe` 摘要执行。摘要仅绑定选项,不证明域名所有权。此独立 CLI 模式支持 `--json`,不读取项目配置,也不会让 `scan()` 或编辑器自动联网。
131
131
 
132
- 仅接受 443 端口的 HTTPS 和简单字面路径,不支持凭据、查询、片段或路径编码。DNS 返回的地址须全部为普通公网地址;连接固定到已验证地址,并保留主机名及 TLS 校验。不跟随重定向,不携带认证信息。检测到代理、网络调试或不安全 TLS 选项时拒绝执行,不绕过配置。DNS 限时 3 秒,单请求 5 秒,响应头上限 16 KiB。
132
+ 仅接受 443 端口的 HTTPS 和简单字面路径,不支持凭据、查询、片段或路径编码。DNS 返回的地址须全部为普通公网地址。若 A 与 AAAA 查询都无法连接配置的 DNS 服务器(常见于 VPN 或代理的虚拟网卡),在同一时限内改用系统解析器;否定回答不回退。连接固定到已验证地址,并保留主机名及 TLS 校验。不跟随重定向,不携带认证信息。检测到代理、网络调试或不安全 TLS 选项时拒绝执行,不绕过配置。DNS 限时 3 秒,单请求 5 秒,响应头上限 16 KiB。
133
133
 
134
134
  可选 `--probe-canary-sha256` 仅对名为 `canship-canary`、`canship-canary.txt` 或 `canship-canary.json` 的资源增加 GET。资源须为 16–4096 字节的专用合成内容;正文仅在内存中计算散列,报告只保留匹配状态与字节数,不保留正文或计算出的摘要。压缩或超限正文判失败。不支持 Supabase/Firebase 管理员密钥或业务记录导出。
135
135
 
136
- 执行会向目标暴露连接 IP 和请求路径,请求仍可能有副作用。响应头、状态码及 canary 可读性均不证明整体应用安全。预览退出 `0`;执行完成且无待审阅观察时退出 `0`,需审阅时退出 `2`,参数无效或执行不完整时退出 `3`。JSON 使用 `probe-plan` 或 `probe-report`,不属于扫描报告格式。真实目标验收尚未完成。
136
+ 执行会向目标暴露连接 IP 和请求路径,请求仍可能有副作用。响应头、状态码及 canary 可读性均不证明整体应用安全。预览退出 `0`;执行完成且无待审阅观察时退出 `0`,需审阅时退出 `2`,参数无效或执行不完整时退出 `3`。JSON 使用 `probe-plan` 或 `probe-report`,不属于扫描报告格式。已在专用 GitHub Pages 目标上完成验收,覆盖 HEAD、OPTIONS、canary 匹配与不匹配、期望拒绝的复核提示及不跟随重定向。
137
137
 
138
138
  地址策略依据:[IANA IPv4](https://www.iana.org/assignments/iana-ipv4-special-registry)、[IANA IPv6](https://www.iana.org/assignments/iana-ipv6-special-registry)及 [Azure 平台地址](https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16)。应用层过滤不能替代网络出口控制。
139
139
 
package/docs/reference.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  [Overview](../README.md) · [简体中文](./reference-zh-CN.md)
4
4
 
5
- This reference covers `0.8.0`. Other versions are listed under [Releases](https://github.com/Tasomei/canship/releases).
5
+ This reference covers `0.8.1`. Other versions are listed under [Releases](https://github.com/Tasomei/canship/releases).
6
6
 
7
7
  ## Server entry points
8
8
 
@@ -129,11 +129,11 @@ Default paths are relative to the scan directory; explicit paths are relative to
129
129
 
130
130
  `--probe=https://app.example.com/status` previews two requests: HEAD and OPTIONS with a fixed probe Origin. Replace the example with an authorized target. Review the target, limits and privacy notice; repeat the same options with the displayed `--confirm-probe` digest to execute. The digest binds options, not domain ownership. This standalone CLI mode supports `--json`; it never reads project configuration or enables networking in `scan()` or the editor.
131
131
 
132
- Only HTTPS on port 443 with a simple literal path is accepted: no credentials, queries, fragments or encoded path components. DNS answers must all be ordinary public addresses; connections are pinned while preserving hostname/TLS verification. No redirects or authenticated requests are made. Configured proxies, network debug settings and insecure TLS overrides are refused rather than bypassed. Limits: DNS 3 seconds, each request 5 seconds, response headers 16 KiB.
132
+ Only HTTPS on port 443 with a simple literal path is accepted: no credentials, queries, fragments or encoded path components. DNS answers must all be ordinary public addresses. If both A and AAAA queries cannot reach the configured DNS servers (common with VPN or proxy adapters), the system resolver is used within the same time limit; negative answers do not fall back. Connections are pinned while preserving hostname/TLS verification. No redirects or authenticated requests are made. Configured proxies, network debug settings and insecure TLS overrides are refused rather than bypassed. Limits: DNS 3 seconds, each request 5 seconds, response headers 16 KiB.
133
133
 
134
134
  Optional `--probe-canary-sha256` adds GET only for a resource named `canship-canary`, `canship-canary.txt` or `canship-canary.json`. Use dedicated synthetic content of 16–4096 bytes. The response is hashed in memory; only match state and byte count are reported, never the body or computed digest. Compressed and oversized bodies fail. No Supabase/Firebase admin key or business-record export is supported.
135
135
 
136
- Execution reveals the connecting IP and requested path to the target; requests can have side effects. Header presence, status codes and a readable canary do not prove general application security. Preview exits `0`; execution exits `0` for completed requests without review items, `2` for observations requiring review, or `3` for invalid/incomplete execution. JSON uses `probe-plan` or `probe-report`, not the scan-report schema. Live-target acceptance remains pending.
136
+ Execution reveals the connecting IP and requested path to the target; requests can have side effects. Header presence, status codes and a readable canary do not prove general application security. Preview exits `0`; execution exits `0` for completed requests without review items, `2` for observations requiring review, or `3` for invalid/incomplete execution. JSON uses `probe-plan` or `probe-report`, not the scan-report schema. Acceptance has been run against a dedicated GitHub Pages target, covering HEAD, OPTIONS, canary matching and mismatch, expected-denial review and an unfollowed redirect.
137
137
 
138
138
  Address policy references: [IANA IPv4](https://www.iana.org/assignments/iana-ipv4-special-registry), [IANA IPv6](https://www.iana.org/assignments/iana-ipv6-special-registry), and [Azure platform address](https://learn.microsoft.com/en-us/azure/virtual-network/what-is-ip-address-168-63-129-16). Application filtering does not replace network egress controls.
139
139
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canship",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "Pre-deployment security checks for JS/TS web apps: offline static scanning for credentials, access rules and unsafe request input, with opt-in deployment probes.",
5
5
  "keywords": [
6
6
  "security",