canship 0.8.0-rc.1 → 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 +45 -61
- package/README.md +47 -63
- package/dist/cli.js +48 -12
- package/dist/index.js +9 -4
- package/docs/reference-zh-CN.md +4 -4
- package/docs/reference.md +4 -4
- package/package.json +2 -2
package/README-zh-CN.md
CHANGED
|
@@ -1,82 +1,69 @@
|
|
|
1
1
|
# canship
|
|
2
2
|
|
|
3
|
-
面向 JavaScript / TypeScript Web
|
|
3
|
+
面向 JavaScript / TypeScript Web 应用的发布前安全检查工具。Canship 在源码中查找凭据泄露、缺失的访问控制及不安全的请求输入处理,并可选择对你拥有的部署进行校验。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
静态扫描在本地运行:只读、不执行项目代码、不发起网络请求。部署校验是独立的可选模式,只访问经你确认的目标。
|
|
6
6
|
|
|
7
|
-
[English](./README.md) · [使用参考](./docs/reference-zh-CN.md) · [npm](https://www.npmjs.com/package/canship)
|
|
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
|
-
>
|
|
9
|
+
> 本文对应 npm `latest` 渠道的 `0.8.1`。其他版本见[发布说明](https://github.com/Tasomei/canship/releases)。
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## 快速开始
|
|
12
12
|
|
|
13
|
-
要求 Node.js
|
|
13
|
+
要求 Node.js 18 及以上,无运行时依赖。
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
npx canship@0.8.
|
|
16
|
+
npx canship@0.8.1
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
扫描指定目录:
|
|
20
20
|
|
|
21
21
|
```powershell
|
|
22
|
-
npx canship@0.8.
|
|
22
|
+
npx canship@0.8.1 "./my-app"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
以下截图使用开发构建及合成数据。
|
|
25
|
+
合成项目的输出示例:
|
|
28
26
|
|
|
29
27
|

|
|
30
28
|
|
|
31
29
|
## 检测范围
|
|
32
30
|
|
|
33
|
-
| 类别 | 级别 |
|
|
31
|
+
| 类别 | 级别 | 内容 |
|
|
34
32
|
|---|:---:|---|
|
|
35
|
-
| 凭据 | `P0` |
|
|
36
|
-
| API 访问 | `P0/P1` | 未识别到鉴权的数据库操作、服务端信任 Supabase `getSession()
|
|
37
|
-
| 数据库规则 | `P1/P2` | Supabase RLS
|
|
38
|
-
| CORS | `P1/P2` |
|
|
39
|
-
|
|
|
33
|
+
| 凭据 | `P0` | 硬编码密钥、公开环境变量中的密钥、Supabase 服务密钥、被 Git 跟踪或存在于历史中的 `.env` 文件 |
|
|
34
|
+
| API 访问 | `P0/P1` | 未识别到鉴权的数据库操作、服务端信任 Supabase `getSession()`、未验签的 Stripe webhook |
|
|
35
|
+
| 数据库规则 | `P1/P2` | 未启用 Supabase RLS、过宽的策略、公开存储列表、开放的 Firebase 规则 |
|
|
36
|
+
| CORS | `P1/P2` | 携带凭据时回显来源或使用通配符 |
|
|
37
|
+
| 代码(Code) | `P1/P2` | 由请求输入拼接的 SQL 与 shell 命令;向调用方指定地址发起的服务端请求和重定向 |
|
|
40
38
|
|
|
41
|
-
路由分析覆盖 Next.js、SvelteKit、Nuxt、Remix / React Router、Astro、Express、Hono、Fastify
|
|
39
|
+
路由分析覆盖 Next.js、SvelteKit、Nuxt、Remix / React Router、Astro、Express、Hono、Fastify 的指定入口,详见[入口及限制](./docs/reference-zh-CN.md#服务端入口)。
|
|
42
40
|
|
|
43
41
|
```powershell
|
|
44
|
-
npx canship@0.8.
|
|
42
|
+
npx canship@0.8.1 --list-rules
|
|
45
43
|
```
|
|
46
44
|
|
|
47
45
|
## 审阅结果
|
|
48
46
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
显示全部置信度及详细证据:
|
|
47
|
+
每条结果标为 `certain`(静态证据充分)或 `likely`(需人工审阅),默认只显示 `certain`。两者都不代表凭据有效或问题可被利用。
|
|
52
48
|
|
|
53
49
|
```powershell
|
|
54
|
-
npx canship@0.8.
|
|
50
|
+
npx canship@0.8.1 --all --verbose
|
|
55
51
|
```
|
|
56
52
|
|
|
57
|
-
生成离线 HTML 报告:
|
|
58
|
-
|
|
59
53
|
```powershell
|
|
60
|
-
npx canship@0.8.
|
|
54
|
+
npx canship@0.8.1 --all --report
|
|
61
55
|
```
|
|
62
56
|
|
|
63
57
|

|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
离线 HTML 报告支持筛选和复制修复提示;`--fix-prompt` 在终端输出同样的说明。报告包含文件路径,也可能包含源码摘录:`--no-excerpts` 去除摘录,`--share-summary` 仅输出计数。可下载[合成示例报告](https://github.com/Tasomei/canship/blob/main/docs/demo.html)查看。
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
| 退出码 | 静态扫描结果 |
|
|
61
|
+
| 退出码 | 含义 |
|
|
70
62
|
|---|---|
|
|
71
|
-
| `0` |
|
|
63
|
+
| `0` | 无结果,且覆盖完整(或经 `--best-effort` 接受不完整覆盖) |
|
|
72
64
|
| `1` | 至少一条 `certain` 的 P0/P1 结果 |
|
|
73
|
-
| `2` |
|
|
74
|
-
| `3` |
|
|
75
|
-
| `130` / `143` | 收到 SIGINT / SIGTERM,不生成扫描报告 |
|
|
76
|
-
|
|
77
|
-
退出码基于规则选择、源码抑制及基线处理后的结果。有结果时优先于覆盖不完整;`--best-effort` 不改变 `1` 或 `2`。须另行检查 JSON 的 `partial`、`errors`、`skipped` 和 `filesScanned`。
|
|
78
|
-
|
|
79
|
-
基线表示接受结果,不代表问题已修复。[基线管理](./docs/reference-zh-CN.md#配置)支持审阅已有记录、选择性接受、理由及到期时间。[报告比较](./docs/reference-zh-CN.md#命令行)提供终端、JSON 和离线 HTML 视图,区分新增、持续存在及本次未再出现,不将结果消失视为修复证明。
|
|
65
|
+
| `2` | 其他结果,包括被隐藏的 `likely` |
|
|
66
|
+
| `3` | 输入无效、工具错误或覆盖不完整 |
|
|
80
67
|
|
|
81
68
|
## 接入 CI
|
|
82
69
|
|
|
@@ -95,41 +82,38 @@ jobs:
|
|
|
95
82
|
with:
|
|
96
83
|
fetch-depth: 0
|
|
97
84
|
persist-credentials: false
|
|
98
|
-
- uses: Tasomei/canship@
|
|
85
|
+
- uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
|
|
99
86
|
with:
|
|
100
|
-
version: '0.8.
|
|
87
|
+
version: '0.8.1'
|
|
101
88
|
honor-ignore-markers: false
|
|
102
89
|
```
|
|
103
90
|
|
|
104
|
-
提交哈希固定 Action
|
|
91
|
+
提交哈希固定 Action 实现,`version` 固定 npm 扫描器版本。Action 不安装项目依赖,只输出计数摘要;扫描不完整或工具出错时始终失败。SARIF 上传需显式开启。
|
|
105
92
|
|
|
106
93
|
| 输入 | 固定 Action 的默认值 | 含义 |
|
|
107
94
|
|---|---|---|
|
|
108
|
-
| `version` | `0.
|
|
109
|
-
| `fail-on` | `blocking` | `blocking
|
|
110
|
-
|
|
111
|
-
扫描不完整或工具错误始终失败。默认不读取项目配置、不上传 SARIF。上传需 `security-events: write` 及代码扫描支持,操作前须审阅报告。不可信 PR 使用 `pull_request`,不要使用 `pull_request_target`。详见 [Action 输入](https://github.com/Tasomei/canship/blob/main/action.yml)。
|
|
95
|
+
| `version` | `0.8.1` | 精确的 npm 扫描器版本;请如上例显式设置 |
|
|
96
|
+
| `fail-on` | `blocking` | `blocking`:`certain` 的 P0/P1;`any`:全部结果;`none`:仅报告 |
|
|
112
97
|
|
|
113
|
-
|
|
98
|
+
完整说明见 [Action 输入](https://github.com/Tasomei/canship/blob/main/action.yml)。
|
|
114
99
|
|
|
115
|
-
|
|
100
|
+
## 扫描之外
|
|
116
101
|
|
|
117
|
-
|
|
102
|
+
- **基线**:接受已审阅的结果,可附理由和到期时间,见[基线管理](./docs/reference-zh-CN.md#配置)。
|
|
103
|
+
- **报告比较**:比较两份已保存的 JSON 报告,列出新增、持续存在和不再出现的结果,见[命令参考](./docs/reference-zh-CN.md#命令行)。
|
|
104
|
+
- **工作区与配置**:独立扫描 monorepo 子项目、排除路径,并用 `--explain-config` 或 `--doctor` 查看生效设置。
|
|
105
|
+
- **模板**:用 `--init` 预览 CI 与 pre-commit 配置。pre-commit 钩子扫描工作区,而非暂存区快照。
|
|
106
|
+
- **部署校验**:`--probe=https://…` 预览少量无认证的 HTTPS 请求,确认计划前不会发出任何请求。使用前请阅读[范围与隐私说明](./docs/reference-zh-CN.md#部署校验)。
|
|
107
|
+
- **API 与编辑器**:[编程接口](./docs/reference-zh-CN.md#api-与结构化输出)及 [VS Code 插件预览版](https://github.com/Tasomei/canship/tree/main/extensions/vscode#readme)。
|
|
118
108
|
|
|
119
|
-
|
|
109
|
+
## 限制
|
|
120
110
|
|
|
121
|
-
|
|
111
|
+
- 静态分析可能漏报或误报有意为之的配置,不评估业务授权、限流或依赖漏洞。
|
|
112
|
+
- 脱敏仅覆盖已识别的密钥格式。详细报告和基线应作为内部资料处理。
|
|
113
|
+
- Google 与 Firebase 的 `AIza…` 密钥属于公开标识符,不单独作为泄露报告。
|
|
122
114
|
|
|
123
|
-
-
|
|
124
|
-
- 脱敏仅覆盖已识别格式,未知敏感值可能保留在摘录中;详细报告及基线应按内部材料处理。
|
|
125
|
-
- Google/Firebase/Maps 的 `AIza…` 密钥按公开标识符处理,不单凭其值判定泄露。
|
|
126
|
-
- 不跟随符号链接;嵌套仓库及子模块须单独扫描。范围内跳过项及分析上限会披露;默认排除的依赖和构建目录不计为覆盖缺口。
|
|
127
|
-
- 显式部署校验会访问已确认目标;安装、评估下载及可选 SARIF 上传也可能联网。静态扫描保持离线。
|
|
128
|
-
|
|
129
|
-
详见[资源上限与覆盖边界](./docs/reference-zh-CN.md#隐私与限制)。
|
|
115
|
+
详见[隐私与覆盖限制](./docs/reference-zh-CN.md#隐私与限制)。
|
|
130
116
|
|
|
131
117
|
## 开发与许可
|
|
132
118
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
[MIT](./LICENSE)。Supabase/Firebase 夹具保留 Apache-2.0,Next.js/`cors` 夹具保留 MIT。
|
|
119
|
+
见[开发参考](./docs/reference-zh-CN.md#开发)。采用 [MIT](./LICENSE) 许可;Supabase 与 Firebase 测试夹具保留 Apache-2.0,Next.js 与 `cors` 夹具保留 MIT。
|
package/README.md
CHANGED
|
@@ -1,84 +1,71 @@
|
|
|
1
1
|
# canship
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Pre-deployment security checks for JavaScript and TypeScript web applications. Canship finds exposed credentials, missing access controls and unsafe handling of request input in your source code, and can optionally probe a deployment you own.
|
|
4
4
|
|
|
5
|
-
Static scans are
|
|
5
|
+
Static scans run locally: they are read-only, execute no project code and make no network requests. Deployment probes are a separate, opt-in mode that contacts only a target you confirm.
|
|
6
6
|
|
|
7
|
-
[简体中文](./README-zh-CN.md) · [Reference](./docs/reference.md) · [npm](https://www.npmjs.com/package/canship)
|
|
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
|
-
>
|
|
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
|
-
Requires Node.js
|
|
13
|
+
Requires Node.js 18 or later; no runtime dependencies.
|
|
14
14
|
|
|
15
15
|
```powershell
|
|
16
|
-
npx canship@0.8.
|
|
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.
|
|
22
|
+
npx canship@0.8.1 "./my-app"
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
The following screenshots use synthetic data from a development build.
|
|
25
|
+
Example output for a synthetic project:
|
|
28
26
|
|
|
29
27
|

|
|
30
28
|
|
|
31
29
|
## Checks
|
|
32
30
|
|
|
33
|
-
| Category | Severity |
|
|
31
|
+
| Category | Severity | Covers |
|
|
34
32
|
|---|:---:|---|
|
|
35
|
-
| Credentials | `P0` | Hardcoded
|
|
33
|
+
| Credentials | `P0` | Hardcoded secrets, secrets in public environment variables, Supabase service keys, `.env` files tracked by Git or present in history |
|
|
36
34
|
| API access | `P0/P1` | Database operations without recognised authentication, server-side trust in Supabase `getSession()`, unverified Stripe webhooks |
|
|
37
|
-
| Database rules | `P1/P2` | Supabase RLS,
|
|
38
|
-
| CORS | `P1/P2` | Reflected or wildcard origins with credentials |
|
|
39
|
-
|
|
|
35
|
+
| Database rules | `P1/P2` | Missing Supabase RLS, permissive policies, public storage listing, open Firebase rules |
|
|
36
|
+
| CORS | `P1/P2` | Reflected or wildcard origins combined with credentials |
|
|
37
|
+
| Code | `P1/P2` | SQL and shell commands built from request input; server-side requests and redirects to caller-chosen URLs |
|
|
40
38
|
|
|
41
|
-
Route analysis
|
|
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).
|
|
42
40
|
|
|
43
41
|
```powershell
|
|
44
|
-
npx canship@0.8.
|
|
42
|
+
npx canship@0.8.1 --list-rules
|
|
45
43
|
```
|
|
46
44
|
|
|
47
|
-
##
|
|
48
|
-
|
|
49
|
-
Reports are in English. `certain` means strong static evidence; `likely` requires review. Tests and examples are downgraded to `likely`. Neither confidence level proves credential validity or exploitability.
|
|
45
|
+
## Reviewing findings
|
|
50
46
|
|
|
51
|
-
|
|
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.
|
|
52
48
|
|
|
53
49
|
```powershell
|
|
54
|
-
npx canship@0.8.
|
|
50
|
+
npx canship@0.8.1 --all --verbose
|
|
55
51
|
```
|
|
56
52
|
|
|
57
|
-
Generate an offline HTML report:
|
|
58
|
-
|
|
59
53
|
```powershell
|
|
60
|
-
npx canship@0.8.
|
|
54
|
+
npx canship@0.8.1 --all --report
|
|
61
55
|
```
|
|
62
56
|
|
|
63
57
|

|
|
64
58
|
|
|
65
|
-
|
|
59
|
+
The offline HTML report supports filtering and copyable repair prompts; `--fix-prompt` prints the same instructions in the terminal. Reports include file paths and may include source excerpts: use `--no-excerpts` to omit excerpts, or `--share-summary` for counts only. A [synthetic sample report](https://github.com/Tasomei/canship/blob/main/docs/demo.html) is available for download.
|
|
66
60
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
| Exit | Static scan result |
|
|
61
|
+
| Exit code | Meaning |
|
|
70
62
|
|---|---|
|
|
71
|
-
| `0` | No findings
|
|
63
|
+
| `0` | No findings, with complete coverage (or incomplete coverage accepted via `--best-effort`) |
|
|
72
64
|
| `1` | At least one `certain` P0/P1 finding |
|
|
73
|
-
| `2` | Other findings, including hidden `likely`
|
|
74
|
-
| `3` | Invalid
|
|
75
|
-
| `130` / `143` | Interrupted by SIGINT / SIGTERM; no scan report generated |
|
|
76
|
-
|
|
77
|
-
Status is calculated after rule selection, source suppressions and baselines. Findings take precedence over incomplete coverage; `--best-effort` never changes `1` or `2`. Check JSON `partial`, `errors`, `skipped` and `filesScanned` separately.
|
|
65
|
+
| `2` | Other findings, including hidden `likely` findings |
|
|
66
|
+
| `3` | Invalid input, tool error, or incomplete coverage |
|
|
78
67
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
## Connect to CI
|
|
68
|
+
## Continuous integration
|
|
82
69
|
|
|
83
70
|
Save as `.github/workflows/canship.yml`:
|
|
84
71
|
|
|
@@ -95,41 +82,38 @@ jobs:
|
|
|
95
82
|
with:
|
|
96
83
|
fetch-depth: 0
|
|
97
84
|
persist-credentials: false
|
|
98
|
-
- uses: Tasomei/canship@
|
|
85
|
+
- uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
|
|
99
86
|
with:
|
|
100
|
-
version: '0.8.
|
|
87
|
+
version: '0.8.1'
|
|
101
88
|
honor-ignore-markers: false
|
|
102
89
|
```
|
|
103
90
|
|
|
104
|
-
The hash pins the Action
|
|
91
|
+
The commit hash pins the Action and `version` pins the npm scanner. The Action does not install project dependencies and writes a counts-only job summary; it always fails on incomplete scans or tool errors. SARIF upload is opt-in.
|
|
105
92
|
|
|
106
|
-
| Input |
|
|
93
|
+
| Input | Default in the pinned Action | Meaning |
|
|
107
94
|
|---|---|---|
|
|
108
|
-
| `version` | `0.
|
|
109
|
-
| `fail-on` | `blocking` | `blocking`: certain P0/P1; `any`: all findings; `none`: report only |
|
|
110
|
-
|
|
111
|
-
Incomplete scans and tool errors always fail. Project configuration and SARIF upload are disabled by default. Upload requires `security-events: write` and code scanning support; review reports first. Use `pull_request`, not `pull_request_target`, for untrusted PRs. See [Action inputs](https://github.com/Tasomei/canship/blob/main/action.yml).
|
|
95
|
+
| `version` | `0.8.1` | Exact npm scanner version; set explicitly as above |
|
|
96
|
+
| `fail-on` | `blocking` | `blocking`: `certain` P0/P1; `any`: all findings; `none`: report only |
|
|
112
97
|
|
|
113
|
-
|
|
98
|
+
See all [Action inputs](https://github.com/Tasomei/canship/blob/main/action.yml).
|
|
114
99
|
|
|
115
|
-
|
|
100
|
+
## Beyond scanning
|
|
116
101
|
|
|
117
|
-
|
|
102
|
+
- **Baselines**: accept reviewed findings with an optional reason and expiry date. See [baseline management](./docs/reference.md#configuration).
|
|
103
|
+
- **Report comparison**: compare two saved JSON reports to list added, persisting and no-longer-observed findings. See the [CLI reference](./docs/reference.md#cli).
|
|
104
|
+
- **Workspaces and configuration**: scan monorepo packages independently, exclude paths, and inspect effective settings with `--explain-config` or `--doctor`.
|
|
105
|
+
- **Templates**: preview CI and pre-commit setups with `--init`. The pre-commit hook scans the working tree, not the staged snapshot.
|
|
106
|
+
- **Deployment probes**: `--probe=https://…` previews a small set of unauthenticated HTTPS requests; nothing is sent until you confirm the plan. Review the [scope and privacy notes](./docs/reference.md#deployment-probes) first.
|
|
107
|
+
- **API and editor**: a [programmatic API](./docs/reference.md#api-and-structured-output) and a [VS Code extension preview](https://github.com/Tasomei/canship/tree/main/extensions/vscode#readme).
|
|
118
108
|
|
|
119
|
-
|
|
109
|
+
## Limitations
|
|
120
110
|
|
|
121
|
-
|
|
111
|
+
- Static analysis can miss issues or flag intentional configurations. It does not assess business authorisation, rate limiting or dependency vulnerabilities.
|
|
112
|
+
- Redaction covers recognised secret formats only. Treat detailed reports and baselines as internal material.
|
|
113
|
+
- Google and Firebase `AIza…` keys are public identifiers and are not reported as leaks on their own.
|
|
122
114
|
|
|
123
|
-
|
|
124
|
-
- Redaction covers recognised formats only. Unknown sensitive values may remain in excerpts; detailed reports and baselines should be treated as internal material.
|
|
125
|
-
- Google/Firebase/Maps `AIza…` keys are public identifiers, not leak evidence alone.
|
|
126
|
-
- Symbolic links are not followed. Nested repositories and submodules need separate scans. In-scope skips and analysis limits are disclosed; default dependency/build exclusions are not coverage gaps.
|
|
127
|
-
- Explicit deployment probes contact the approved target; installation, evaluation downloads and optional SARIF upload may also use the network. Static scans remain offline.
|
|
128
|
-
|
|
129
|
-
See [resource bounds and coverage limits](./docs/reference.md#privacy-and-limits).
|
|
115
|
+
See [privacy and coverage limits](./docs/reference.md#privacy-and-limits).
|
|
130
116
|
|
|
131
117
|
## Development and license
|
|
132
118
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
[MIT](./LICENSE). Supabase/Firebase fixtures retain Apache-2.0; Next.js/`cors` fixtures retain MIT.
|
|
119
|
+
See the [development reference](./docs/reference.md#development). Licensed under [MIT](./LICENSE); Supabase and Firebase test fixtures retain Apache-2.0, and Next.js and `cors` fixtures retain MIT.
|
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.
|
|
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";
|
|
@@ -5135,6 +5135,10 @@ function serverIndex(files) {
|
|
|
5135
5135
|
const root = sites.find((other) => other.a === a && other.range === null && other.name === arg.text && !other.root);
|
|
5136
5136
|
return root ? byRoot.get(root) : void 0;
|
|
5137
5137
|
};
|
|
5138
|
+
const fileRoutes = (file) => [.../* @__PURE__ */ new Set([
|
|
5139
|
+
...byRouterFile.get(file) ?? [],
|
|
5140
|
+
...sites.filter((other) => other.a.file === file && other.range === null && !other.root).flatMap((other) => byRoot.get(other) ?? [])
|
|
5141
|
+
])];
|
|
5138
5142
|
for (const site of sites) {
|
|
5139
5143
|
if (site.framework === "fastify") continue;
|
|
5140
5144
|
const { a } = site;
|
|
@@ -5143,7 +5147,7 @@ function serverIndex(files) {
|
|
|
5143
5147
|
if (site.framework === "hono" && !use.mount) continue;
|
|
5144
5148
|
use.args.forEach((arg, index2) => {
|
|
5145
5149
|
const target = mountedFile(a, arg, files);
|
|
5146
|
-
const routes = target && target !== a.file ?
|
|
5150
|
+
const routes = target && target !== a.file ? fileRoutes(target) : localRoutes(a, arg);
|
|
5147
5151
|
if (!routes || routes.length === 0) return;
|
|
5148
5152
|
const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
|
|
5149
5153
|
const covers2 = (other) => site.framework === "hono" ? other.prefix === null || /\*$/.test(other.prefix) && pathMatches(other.prefix, use.prefix, "hono") : pathMatches(other.prefix, use.prefix, site.framework);
|
|
@@ -5263,7 +5267,8 @@ function middlewareDefinition(ref, files) {
|
|
|
5263
5267
|
const local = member === null ? findFunction(a, root) : null;
|
|
5264
5268
|
if (local) return { name: callee, range: local, spec: null, inline };
|
|
5265
5269
|
const target = importOf(a, root, files);
|
|
5266
|
-
const
|
|
5270
|
+
const exported = member ?? (target && target.name !== "default" ? target.name : null);
|
|
5271
|
+
const range = target ? importedFunction(target, member) ?? (member ? propertyValue(analyse(target.file), member) : null) ?? (exported ? exportedMember(target.file, exported, files, 0) : null) : null;
|
|
5267
5272
|
return { name: callee, range, spec: null, inline };
|
|
5268
5273
|
}
|
|
5269
5274
|
function propertyValue(a, name) {
|
|
@@ -8133,8 +8138,8 @@ var InputFlow = class {
|
|
|
8133
8138
|
if (expr.length > MAX_EXPRESSION) this.limited = true;
|
|
8134
8139
|
const text = expr.trim();
|
|
8135
8140
|
if (text === "" || CLEAN_EXPRESSION.some((pattern) => pattern.test(text)) || isComparison(text)) return null;
|
|
8136
|
-
const
|
|
8137
|
-
if (
|
|
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;
|
|
8138
8143
|
const observed = expr.replace(/\[\s*['"][^'"]*['"]\s*\]/g, (part, offset) => {
|
|
8139
8144
|
const raw = this.source.slice(exprAt + offset, exprAt + offset + part.length);
|
|
8140
8145
|
const key = /^\[\s*['"]([A-Za-z_$][\w$]*)['"]\s*\]$/.exec(raw)?.[1];
|
|
@@ -11731,7 +11736,7 @@ function openReport(path, onError, options = {}) {
|
|
|
11731
11736
|
}
|
|
11732
11737
|
|
|
11733
11738
|
// src/build-info.ts
|
|
11734
|
-
var VERSION = true ? "0.8.
|
|
11739
|
+
var VERSION = true ? "0.8.1" : "0.0.0-dev";
|
|
11735
11740
|
var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
|
|
11736
11741
|
function getBuildInfo() {
|
|
11737
11742
|
return { ...identity };
|
|
@@ -12067,7 +12072,7 @@ ${matrix} runs-on: ubuntu-latest
|
|
|
12067
12072
|
with:
|
|
12068
12073
|
fetch-depth: 0
|
|
12069
12074
|
persist-credentials: false
|
|
12070
|
-
- uses: Tasomei/canship@
|
|
12075
|
+
- uses: Tasomei/canship@8f09ad58fca369060e5333b325765ff7870aa92f
|
|
12071
12076
|
with:
|
|
12072
12077
|
version: '${version}'
|
|
12073
12078
|
${target} honor-ignore-markers: false
|
|
@@ -12756,7 +12761,7 @@ function createProbePlan(input, expectAuth = false, canarySha256) {
|
|
|
12756
12761
|
}
|
|
12757
12762
|
|
|
12758
12763
|
// src/probe-network.ts
|
|
12759
|
-
import { Resolver } from "dns/promises";
|
|
12764
|
+
import { Resolver, lookup } from "dns/promises";
|
|
12760
12765
|
import { isIP as isIP2 } from "net";
|
|
12761
12766
|
import { Agent, request } from "https";
|
|
12762
12767
|
import { createHash as createHash7 } from "crypto";
|
|
@@ -12773,10 +12778,38 @@ function checkProbeRuntime(env = process.env, args = process.execArgv) {
|
|
|
12773
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.");
|
|
12774
12779
|
}
|
|
12775
12780
|
}
|
|
12776
|
-
|
|
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 })) {
|
|
12777
12809
|
checkProbeCancelled(signal);
|
|
12778
12810
|
const literal2 = isIP2(hostname);
|
|
12779
12811
|
if (literal2 === 4 || literal2 === 6) return [{ address: hostname, family: literal2 }];
|
|
12812
|
+
const started = Date.now();
|
|
12780
12813
|
const resolver = createResolver();
|
|
12781
12814
|
let timedOut = false, stopped = false;
|
|
12782
12815
|
const cancel = () => {
|
|
@@ -12802,11 +12835,14 @@ async function resolveProbeAddresses(hostname, signal, createResolver = () => ne
|
|
|
12802
12835
|
})
|
|
12803
12836
|
]);
|
|
12804
12837
|
checkProbeCancelled(signal);
|
|
12805
|
-
if (timedOut) throw
|
|
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
|
+
}
|
|
12806
12842
|
const addresses = [];
|
|
12807
12843
|
for (const [result, family] of [[a, 4], [aaaa, 6]]) {
|
|
12808
12844
|
if (result.status === "fulfilled") addresses.push(...result.value.map((address) => ({ address, family })));
|
|
12809
|
-
else if (!["ENODATA", "ENOTFOUND"].includes(String(result.reason?.code))) throw
|
|
12845
|
+
else if (!["ENODATA", "ENOTFOUND"].includes(String(result.reason?.code))) throw DNS_FAILED();
|
|
12810
12846
|
}
|
|
12811
12847
|
return addresses;
|
|
12812
12848
|
} finally {
|
|
@@ -13442,7 +13478,7 @@ function parseArgs(argv) {
|
|
|
13442
13478
|
return args;
|
|
13443
13479
|
}
|
|
13444
13480
|
var HELP = `
|
|
13445
|
-
${bold("canship")} \u2014
|
|
13481
|
+
${bold("canship")} \u2014 pre-deployment security checks for JS/TS apps: offline static scans, opt-in deployment probes
|
|
13446
13482
|
|
|
13447
13483
|
${bold("Usage")}
|
|
13448
13484
|
npx canship [path]
|
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.
|
|
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";
|
|
@@ -5085,6 +5085,10 @@ function serverIndex(files) {
|
|
|
5085
5085
|
const root = sites.find((other) => other.a === a && other.range === null && other.name === arg.text && !other.root);
|
|
5086
5086
|
return root ? byRoot.get(root) : void 0;
|
|
5087
5087
|
};
|
|
5088
|
+
const fileRoutes = (file) => [.../* @__PURE__ */ new Set([
|
|
5089
|
+
...byRouterFile.get(file) ?? [],
|
|
5090
|
+
...sites.filter((other) => other.a.file === file && other.range === null && !other.root).flatMap((other) => byRoot.get(other) ?? [])
|
|
5091
|
+
])];
|
|
5088
5092
|
for (const site of sites) {
|
|
5089
5093
|
if (site.framework === "fastify") continue;
|
|
5090
5094
|
const { a } = site;
|
|
@@ -5093,7 +5097,7 @@ function serverIndex(files) {
|
|
|
5093
5097
|
if (site.framework === "hono" && !use.mount) continue;
|
|
5094
5098
|
use.args.forEach((arg, index2) => {
|
|
5095
5099
|
const target = mountedFile(a, arg, files);
|
|
5096
|
-
const routes = target && target !== a.file ?
|
|
5100
|
+
const routes = target && target !== a.file ? fileRoutes(target) : localRoutes(a, arg);
|
|
5097
5101
|
if (!routes || routes.length === 0) return;
|
|
5098
5102
|
const isMiddleware = (x) => mountedFile(a, x, files) === null && localRoutes(a, x) === void 0;
|
|
5099
5103
|
const covers2 = (other) => site.framework === "hono" ? other.prefix === null || /\*$/.test(other.prefix) && pathMatches(other.prefix, use.prefix, "hono") : pathMatches(other.prefix, use.prefix, site.framework);
|
|
@@ -5213,7 +5217,8 @@ function middlewareDefinition(ref, files) {
|
|
|
5213
5217
|
const local = member === null ? findFunction(a, root) : null;
|
|
5214
5218
|
if (local) return { name: callee, range: local, spec: null, inline };
|
|
5215
5219
|
const target = importOf(a, root, files);
|
|
5216
|
-
const
|
|
5220
|
+
const exported = member ?? (target && target.name !== "default" ? target.name : null);
|
|
5221
|
+
const range = target ? importedFunction(target, member) ?? (member ? propertyValue(analyse(target.file), member) : null) ?? (exported ? exportedMember(target.file, exported, files, 0) : null) : null;
|
|
5217
5222
|
return { name: callee, range, spec: null, inline };
|
|
5218
5223
|
}
|
|
5219
5224
|
function propertyValue(a, name) {
|
|
@@ -9982,7 +9987,7 @@ function summarize(result) {
|
|
|
9982
9987
|
}
|
|
9983
9988
|
|
|
9984
9989
|
// src/build-info.ts
|
|
9985
|
-
var VERSION = true ? "0.8.
|
|
9990
|
+
var VERSION = true ? "0.8.1" : "0.0.0-dev";
|
|
9986
9991
|
var identity = typeof define_CANSHIP_BUILD_INFO_default === "object" ? define_CANSHIP_BUILD_INFO_default : { version: VERSION, channel: "development", revision: null, dirty: null };
|
|
9987
9992
|
function getBuildInfo() {
|
|
9988
9993
|
return { ...identity };
|
package/docs/reference-zh-CN.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[概览](../README-zh-CN.md) · [English](./reference.md)
|
|
4
4
|
|
|
5
|
-
本文对应
|
|
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
|
|
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
|
|
|
@@ -158,7 +158,7 @@ Canship v3 指纹用于自身基线及报告比较中的结果识别。[GitHub c
|
|
|
158
158
|
|
|
159
159
|
## 兼容性
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
CI 应固定扫描器精确版本,并使用对应发行版文档;1.0 前升级须检查发行说明并重新生成报告。
|
|
162
162
|
|
|
163
163
|
1.0 的契约约定:公开 CLI 参数、退出码语义或 API 导出类型发生破坏性变更时升级主版本;报告或基线格式不兼容时升级格式版本并提供迁移说明。包版本与数据格式版本独立:扫描 JSON 为 v1,新基线为 v4(可读 v2/v3),稳定指纹为 v3,SARIF 为 2.1.0。须按操作 `kind` 和 `schemaVersion` 分派 JSON,普通扫描报告没有 `kind`。接受已约定的可选新增字段及未知诊断代码;不支持的格式版本应拒绝处理,不能当作无问题结果。
|
|
164
164
|
|
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
|
|
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;
|
|
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.
|
|
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
|
|
|
@@ -158,7 +158,7 @@ Build identity distinguishes development, prerelease, and release artifacts. Onl
|
|
|
158
158
|
|
|
159
159
|
## Compatibility
|
|
160
160
|
|
|
161
|
-
|
|
161
|
+
Pin exact scanner versions in CI and consult the documentation for that release. Before 1.0, inspect release notes and regenerate reports when upgrading.
|
|
162
162
|
|
|
163
163
|
For the 1.0 contract, breaking changes to public CLI options, exit semantics or exported API types require a major release. Incompatible report or baseline formats require a format-version change and migration guidance. Package versions and data-format versions are separate: scan JSON is v1, new baselines are v4 (v2/v3 readable), stable fingerprints are v3, and SARIF is 2.1.0. Dispatch JSON by operation `kind` and `schemaVersion`; ordinary scan reports have no `kind`. Accept documented optional additions and unknown diagnostic codes, but reject unsupported format versions rather than interpreting them as clean results.
|
|
164
164
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canship",
|
|
3
|
-
"version": "0.8.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.1",
|
|
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",
|
|
7
7
|
"audit",
|