canship 0.3.0 → 0.3.2
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 +9 -8
- package/README.md +9 -8
- package/dist/cli.js +205 -57
- package/package.json +1 -1
package/README-zh-CN.md
CHANGED
|
@@ -22,10 +22,10 @@ npx canship .
|
|
|
22
22
|
| Git 跟踪及历史 `.env` 文件中的凭据或疑似私密值 | P0 |
|
|
23
23
|
| Supabase 迁移记录中未启用行级安全(RLS)的表 | P1 |
|
|
24
24
|
| Firebase 无条件访问及固定日期测试规则 | P1 |
|
|
25
|
-
|
|
|
25
|
+
| 服务端路由数据操作未识别到鉴权 | P0 / P1 |
|
|
26
26
|
| 携带凭据的 CORS 来源回显或通配符配置 | P1 / P2 |
|
|
27
27
|
|
|
28
|
-
支持 OpenAI、Anthropic、AWS、Stripe、GitHub、npm、Slack、SendGrid 等凭据格式及常见前端公开环境变量前缀。API
|
|
28
|
+
支持 OpenAI、Anthropic、AWS、Stripe、GitHub、npm、Slack、SendGrid 等凭据格式及常见前端公开环境变量前缀。API 鉴权检查覆盖以下框架的服务端路由:Next.js(App Router 与 Pages Router 的 `/api`)、SvelteKit(`+server` 端点)、Nuxt(`server/api` 与 `server/routes`)、Remix 与 React Router(`app/routes` 中导出 `loader` 或 `action` 的模块)、Astro(`src/pages` 中的端点),支持路由组和工作区应用。不检查 SvelteKit 的页面 load 与表单 action。SvelteKit `hooks.server` 或 Nuxt `server/middleware` 中的鉴权会降低结果置信度而不是直接隐藏结果,因为它覆盖哪些路由由代码决定。
|
|
29
29
|
|
|
30
30
|
置信度分为确定(`certain`)和疑似(`likely`),仅描述静态证据,不验证凭据有效性或线上状态。默认只展示确定结果;隐藏的疑似结果仍影响退出码。
|
|
31
31
|
|
|
@@ -46,6 +46,7 @@ npx canship .
|
|
|
46
46
|
| `--only=ids` | 仅执行匹配规则,支持逗号分隔及重复参数 |
|
|
47
47
|
| `--skip=ids` | 排除匹配规则,支持逗号分隔及重复参数 |
|
|
48
48
|
| `--no-config` | 忽略项目配置 |
|
|
49
|
+
| `--no-ignore-markers` | 不遵从被扫描源码中的忽略标记 |
|
|
49
50
|
| `--list-rules` | 列出规则及限制,不扫描;支持 `--json` |
|
|
50
51
|
| `--no-excerpts` | 所有报告省略源码摘录,不改变结果和退出码 |
|
|
51
52
|
| `-h`, `--help` | 显示帮助 |
|
|
@@ -74,7 +75,7 @@ JSON 使用独立于包版本的 `schemaVersion: 1`,npm 包附带 [结构定
|
|
|
74
75
|
|
|
75
76
|
保存为 `.github/workflows/canship.yml`,在推送和 PR 时扫描并生成统计摘要。安装扫描器需要联网,扫描不联网;不安装或执行项目依赖,默认不上传 SARIF。
|
|
76
77
|
|
|
77
|
-
示例固定 Action
|
|
78
|
+
示例固定 Action 提交,显式安装 npm 版 `0.3.1`;`version` 不使用仓库中的未发布源码。Action 兼容 0.2.1 无 `schemaVersion` 的报告。
|
|
78
79
|
|
|
79
80
|
```yaml
|
|
80
81
|
name: canship
|
|
@@ -89,15 +90,15 @@ jobs:
|
|
|
89
90
|
with:
|
|
90
91
|
fetch-depth: 0
|
|
91
92
|
persist-credentials: false
|
|
92
|
-
- uses: Tasomei/canship@
|
|
93
|
+
- uses: Tasomei/canship@2d33cce0ad8439e34f01f5218fdaaf4657215e3d
|
|
93
94
|
with:
|
|
94
|
-
version: '0.
|
|
95
|
+
version: '0.3.1'
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
| 输入 | 默认值 | 说明 |
|
|
98
99
|
|---|---|---|
|
|
99
100
|
| `path` | `.` | 检出目录内的扫描路径 |
|
|
100
|
-
| `version` | `0.
|
|
101
|
+
| `version` | `0.3.1` | 精确 npm 版本,不接受范围或标签 |
|
|
101
102
|
| `fail-on` | `blocking` | `blocking`:确定的 P0/P1;`any`:全部结果;`none`:仅报告结果 |
|
|
102
103
|
| `only` / `skip` | 未设置 | 互斥,逗号分隔的规则选择器 |
|
|
103
104
|
| `baseline` | 未设置 | 相对扫描目录的已有基线 |
|
|
@@ -120,7 +121,7 @@ jobs:
|
|
|
120
121
|
}
|
|
121
122
|
```
|
|
122
123
|
|
|
123
|
-
命令行参数优先;`only`、`skip` 互斥,接受规则 ID 或命名空间。未选规则不执行,`ruleSelection.removed`
|
|
124
|
+
命令行参数优先;`only`、`skip` 互斥,接受规则 ID 或命名空间。未选规则不执行,`ruleSelection.removed` 仅统计已执行规则中被过滤的结果。扫描不可信项目时同时使用 `--no-config --no-ignore-markers`,二者均由被扫描项目控制;`bestEffort` 仅限命令行设置。
|
|
124
125
|
|
|
125
126
|
### 忽略标记
|
|
126
127
|
|
|
@@ -131,7 +132,7 @@ jobs:
|
|
|
131
132
|
const corsOptions = { origin: '*', credentials: true }
|
|
132
133
|
```
|
|
133
134
|
|
|
134
|
-
|
|
135
|
+
报告披露忽略、规则筛选和基线抑制信息;主动排除不标记为未完成。标记可使退出码降为 `0`;`--no-config` 不影响标记,`--no-ignore-markers` 使两种标记均失效。
|
|
135
136
|
|
|
136
137
|
### 基线
|
|
137
138
|
|
package/README.md
CHANGED
|
@@ -22,10 +22,10 @@ Requires Node.js ≥18; no runtime dependencies. `npx` may download the package;
|
|
|
22
22
|
| Credentials or suspected private values in Git-tracked and historical `.env` files | P0 |
|
|
23
23
|
| Supabase tables without Row Level Security (RLS) in migrations | P1 |
|
|
24
24
|
| Firebase unconditional access and date-based test rules | P1 |
|
|
25
|
-
|
|
|
25
|
+
| Server route data operations without recognised authentication | P0 / P1 |
|
|
26
26
|
| Credentialed CORS with reflected or wildcard origins | P1 / P2 |
|
|
27
27
|
|
|
28
|
-
Supports OpenAI, Anthropic, AWS, Stripe, GitHub, npm, Slack, SendGrid, and other credential formats, plus common frontend public environment prefixes. API authentication checks cover
|
|
28
|
+
Supports OpenAI, Anthropic, AWS, Stripe, GitHub, npm, Slack, SendGrid, and other credential formats, plus common frontend public environment prefixes. API authentication checks cover server routes in Next.js (`/api` in the App and Pages Router), SvelteKit (`+server` endpoints), Nuxt (`server/api` and `server/routes`), Remix and React Router (modules in `app/routes` exporting `loader` or `action`), and Astro (endpoints in `src/pages`), including route groups and workspace applications. SvelteKit page loads and form actions are not checked. An authentication check in SvelteKit `hooks.server` or Nuxt `server/middleware` lowers confidence instead of suppressing findings, because the routes it covers are decided in code.
|
|
29
29
|
|
|
30
30
|
Confidence is `certain` or `likely`, describing static evidence rather than credential validity or deployed state. Only certain findings are shown by default; hidden likely findings still affect the exit code.
|
|
31
31
|
|
|
@@ -46,6 +46,7 @@ Omitting the path scans the current directory.
|
|
|
46
46
|
| `--only=ids` | Run matching rules; comma-separated and repeatable |
|
|
47
47
|
| `--skip=ids` | Exclude matching rules; comma-separated and repeatable |
|
|
48
48
|
| `--no-config` | Ignore project configuration |
|
|
49
|
+
| `--no-ignore-markers` | Disregard ignore markers in scanned source |
|
|
49
50
|
| `--list-rules` | List rules and limits without scanning; supports `--json` |
|
|
50
51
|
| `--no-excerpts` | Omit source excerpts from every report; preserve findings and exit status |
|
|
51
52
|
| `-h`, `--help` | Show help |
|
|
@@ -74,7 +75,7 @@ JSON uses `schemaVersion: 1`, independent of the package version; the npm packag
|
|
|
74
75
|
|
|
75
76
|
Save as `.github/workflows/canship.yml` to scan on pushes and pull requests, with a counts-only summary. Scanner installation requires network access; scanning does not. Project dependencies are not installed or executed, and SARIF upload is disabled by default.
|
|
76
77
|
|
|
77
|
-
The example pins the Action commit and installs npm version `0.
|
|
78
|
+
The example pins the Action commit and explicitly installs npm version `0.3.1`; `version` does not use unreleased repository source. The Action accepts 0.2.1 reports without `schemaVersion`.
|
|
78
79
|
|
|
79
80
|
```yaml
|
|
80
81
|
name: canship
|
|
@@ -89,15 +90,15 @@ jobs:
|
|
|
89
90
|
with:
|
|
90
91
|
fetch-depth: 0
|
|
91
92
|
persist-credentials: false
|
|
92
|
-
- uses: Tasomei/canship@
|
|
93
|
+
- uses: Tasomei/canship@2d33cce0ad8439e34f01f5218fdaaf4657215e3d
|
|
93
94
|
with:
|
|
94
|
-
version: '0.
|
|
95
|
+
version: '0.3.1'
|
|
95
96
|
```
|
|
96
97
|
|
|
97
98
|
| Input | Default | Meaning |
|
|
98
99
|
|---|---|---|
|
|
99
100
|
| `path` | `.` | Directory within the checkout |
|
|
100
|
-
| `version` | `0.
|
|
101
|
+
| `version` | `0.3.1` | Exact npm version; no ranges or tags |
|
|
101
102
|
| `fail-on` | `blocking` | `blocking`: certain P0/P1; `any`: all findings; `none`: findings only reported |
|
|
102
103
|
| `only` / `skip` | unset | Mutually exclusive, comma-separated rule selectors |
|
|
103
104
|
| `baseline` | unset | Existing baseline relative to the scanned directory |
|
|
@@ -120,7 +121,7 @@ Place `canship.config.json` in the scanned directory. Supported keys: `baseline`
|
|
|
120
121
|
}
|
|
121
122
|
```
|
|
122
123
|
|
|
123
|
-
CLI options take precedence. `only` and `skip` are mutually exclusive and accept rule IDs or namespaces. Unselected rules do not run; `ruleSelection.removed` counts filtered findings only from executed rules.
|
|
124
|
+
CLI options take precedence. `only` and `skip` are mutually exclusive and accept rule IDs or namespaces. Unselected rules do not run; `ruleSelection.removed` counts filtered findings only from executed rules. For untrusted projects use `--no-config --no-ignore-markers`; the scanned project controls both. `bestEffort` is CLI-only.
|
|
124
125
|
|
|
125
126
|
### Ignore markers
|
|
126
127
|
|
|
@@ -131,7 +132,7 @@ A standalone `canship-ignore-file` comment excludes a file; `canship-ignore-next
|
|
|
131
132
|
const corsOptions = { origin: '*', credentials: true }
|
|
132
133
|
```
|
|
133
134
|
|
|
134
|
-
Reports disclose exclusions, rule selection, and baseline suppression. Deliberate exclusions do not mark the scan incomplete.
|
|
135
|
+
Reports disclose exclusions, rule selection, and baseline suppression. Deliberate exclusions do not mark the scan incomplete. Markers can lower the exit code to `0`; `--no-config` does not affect them, `--no-ignore-markers` disables both kinds.
|
|
135
136
|
|
|
136
137
|
### Baselines
|
|
137
138
|
|
package/dist/cli.js
CHANGED
|
@@ -779,7 +779,7 @@ function isExampleContext(relPath) {
|
|
|
779
779
|
if (/\.(test|spec)\.[jt]sx?$/i.test(name)) return true;
|
|
780
780
|
return false;
|
|
781
781
|
}
|
|
782
|
-
function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}) {
|
|
782
|
+
function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root), limits = {}, honorIgnoreMarkers = true) {
|
|
783
783
|
const skipped = [];
|
|
784
784
|
const ignored = [];
|
|
785
785
|
const fromGit = isGitRepo ? listViaGit(root, gitExecutable) : null;
|
|
@@ -863,7 +863,7 @@ function collectFiles(root, isGitRepo, gitExecutable = resolveGitExecutable(root
|
|
|
863
863
|
continue;
|
|
864
864
|
}
|
|
865
865
|
const lines = content.split(/\r?\n/);
|
|
866
|
-
if (hasIgnoreMarker(lines)) {
|
|
866
|
+
if (honorIgnoreMarkers && hasIgnoreMarker(lines)) {
|
|
867
867
|
ignored.push(relPath);
|
|
868
868
|
continue;
|
|
869
869
|
}
|
|
@@ -2301,25 +2301,69 @@ var firebaseRulesRule = {
|
|
|
2301
2301
|
import { posix } from "path";
|
|
2302
2302
|
var APP_ROUTER = /(?:^|\/)app\/api\/(?:.+\/)?route\.[mc]?[jt]sx?$/;
|
|
2303
2303
|
var PAGES_ROUTER = /(?:^|\/)pages\/api\/.+\.[mc]?[jt]sx?$/;
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2304
|
+
var SVELTEKIT_ENDPOINT = /^(.*?\/)?src\/routes\/(?:(.*)\/)?\+server\.[mc]?[jt]s$/;
|
|
2305
|
+
var NUXT_ROUTE = /^(.*?\/)?server\/(api|routes)\/(.+)\.[mc]?[jt]s$/;
|
|
2306
|
+
var H3_HANDLER = /\b(?:defineEventHandler|eventHandler|defineCachedEventHandler|defineLazyEventHandler)\s*\(/;
|
|
2307
|
+
var NUXT_METHOD_SUFFIX = /\.(?:get|post|put|patch|delete|head|options|connect|trace)$/i;
|
|
2308
|
+
var REMIX_ROUTE = /^(.*?\/)?app\/routes\/(.+)\.[mc]?[jt]sx?$/;
|
|
2309
|
+
var REMIX_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:loader|action)\b|\bexport\s+const\s+(?:loader|action)\b/;
|
|
2310
|
+
var ASTRO_ENDPOINT = /^(.*?\/)?src\/pages\/(.+)\.[mc]?[jt]s$/;
|
|
2311
|
+
var ASTRO_EXPORT = /\bexport\s+(?:async\s+)?function\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b|\bexport\s+const\s+(?:GET|POST|PUT|PATCH|DELETE|ALL)\b/;
|
|
2307
2312
|
function routePathOf(path) {
|
|
2308
2313
|
return path.replace(/(^|\/)\([^/]+\)(?=\/)/g, "$1").replace(/\/{2,}/g, "/");
|
|
2309
2314
|
}
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2315
|
+
function routeOf(file) {
|
|
2316
|
+
const path = routePathOf(file.path);
|
|
2317
|
+
if (APP_ROUTER.test(path) || PAGES_ROUTER.test(file.path)) {
|
|
2318
|
+
const scope = /^(.*?)(?:src\/)?(?:app|pages)\/api\//.exec(path)?.[1] ?? "";
|
|
2319
|
+
return { file, framework: "next", url: nextRouteUrl(path), scope };
|
|
2320
|
+
}
|
|
2321
|
+
const svelte = SVELTEKIT_ENDPOINT.exec(path);
|
|
2322
|
+
if (svelte) return { file, framework: "sveltekit", url: `/${svelte[2] ?? ""}`, scope: svelte[1] ?? "" };
|
|
2323
|
+
const nuxt = NUXT_ROUTE.exec(path);
|
|
2324
|
+
if (nuxt && H3_HANDLER.test(noiseMaskedOf(file))) {
|
|
2325
|
+
const rest = nuxt[3].replace(NUXT_METHOD_SUFFIX, "").replace(/(?:^|\/)index$/, "");
|
|
2326
|
+
const url = nuxt[2] === "api" ? `/api/${rest}` : `/${rest}`;
|
|
2327
|
+
return { file, framework: "nuxt", url: url.replace(/\/$/, "") || "/", scope: nuxt[1] ?? "" };
|
|
2328
|
+
}
|
|
2329
|
+
const remix = REMIX_ROUTE.exec(path);
|
|
2330
|
+
if (remix && REMIX_EXPORT.test(noiseMaskedOf(file))) {
|
|
2331
|
+
return { file, framework: "remix", url: remixRouteUrl(remix[2]), scope: remix[1] ?? "" };
|
|
2332
|
+
}
|
|
2333
|
+
const astro = ASTRO_ENDPOINT.exec(path);
|
|
2334
|
+
if (astro && ASTRO_EXPORT.test(noiseMaskedOf(file))) {
|
|
2335
|
+
const rest = astro[2].replace(/(?:^|\/)index$/, "");
|
|
2336
|
+
return { file, framework: "astro", url: `/${rest}`, scope: astro[1] ?? "" };
|
|
2337
|
+
}
|
|
2338
|
+
return null;
|
|
2314
2339
|
}
|
|
2315
|
-
function
|
|
2316
|
-
const m = /(?:^|\/)(?:app|pages)\/(api\/.*)$/.exec(
|
|
2340
|
+
function nextRouteUrl(path) {
|
|
2341
|
+
const m = /(?:^|\/)(?:app|pages)\/(api\/.*)$/.exec(path);
|
|
2317
2342
|
if (!m) return `/${path}`;
|
|
2318
2343
|
const url = m[1].replace(/\/route\.[mc]?[jt]sx?$/, "").replace(/\/index\.[mc]?[jt]sx?$/, "").replace(/\.[mc]?[jt]sx?$/, "");
|
|
2319
2344
|
return `/${url}`;
|
|
2320
2345
|
}
|
|
2321
|
-
|
|
2322
|
-
|
|
2346
|
+
function remixRouteUrl(name) {
|
|
2347
|
+
const LITERAL_DOT = "\0";
|
|
2348
|
+
const segments = name.replace(/\/route$/, "").replace(/\[\.\]/g, LITERAL_DOT).split(/[./]/).filter((segment) => segment !== "" && !segment.startsWith("_")).map((segment) => segment.replace(/_$/, "").replace(/[[\]]/g, "").split(LITERAL_DOT).join("."));
|
|
2349
|
+
if (segments[segments.length - 1] === "index") segments.pop();
|
|
2350
|
+
return `/${segments.join("/")}`;
|
|
2351
|
+
}
|
|
2352
|
+
var AUTH_ENDPOINT_NAMES = /^(?:\/api)?\/auth\/(?:sign[-_]?in|sign[-_]?up|sign[-_]?out|log[-_]?in|log[-_]?out|register|session|verify|confirm|reset(?:[-_]password)?|forgot(?:[-_]password)?|magic[-_]?link|otp)$/;
|
|
2353
|
+
var AUTH_CALLBACK = /^(?:\/api)?\/auth\/callback(?:\/[^/]+)?$/;
|
|
2354
|
+
var PROVIDER_CALLBACK = /^(?:\/api)?\/(?:auth|oauth|login|sign-?in)\/[^/]+\/callback$/;
|
|
2355
|
+
var OAUTH_HANDLER = /\bdefineOAuth\w*EventHandler\s*\(/;
|
|
2356
|
+
function isAuthEndpoint(route) {
|
|
2357
|
+
return AUTH_ENDPOINT_NAMES.test(route.url) || AUTH_CALLBACK.test(route.url) || PROVIDER_CALLBACK.test(route.url) || OAUTH_HANDLER.test(noiseMaskedOf(route.file));
|
|
2358
|
+
}
|
|
2359
|
+
var AUTH_ENFORCING_CALL = /\b(?:NextAuth|require(?:Auth|User|Session|Admin)\w*|withAuth|verifyAuth|ensureAuth|assertAuth(?:enticated)?|verifyIdToken|constructEvent)\s*\(/i;
|
|
2360
|
+
var AUTH_CONDITION = /\b(?:session|token|user|userid|user_id|authorization|bearer|jwt|auth|signature|CRON_SECRET|WEBHOOK_SECRET|REVALIDATE_SECRET|ADMIN_SECRET)\b|\blocals\s*\.\s*user\b|\b(?:getUser|getSession|getServerSession|currentUser|getAuth|isAuthenticated|checkAuth|verifyAuth|ensureAuth|verifyIdToken|timingSafeEqual)\s*\(/i;
|
|
2361
|
+
var ACCESS_CHAIN = String.raw`[\w$]+(?:\??\.[\w$]+)*`;
|
|
2362
|
+
var NEGATED_IDENTITY = new RegExp(`^!\\s*${ACCESS_CHAIN}(?:\\s*\\([^=]*\\))?$`);
|
|
2363
|
+
var IDENTITY_IS_EMPTY = new RegExp(`^${ACCESS_CHAIN}\\s*={2,3}\\s*(?:null|undefined|false)$`);
|
|
2364
|
+
var IDENTITY_MISMATCH = new RegExp(`^${ACCESS_CHAIN}\\s*!={1,2}\\s*(?!(?:null|undefined|false)\\b)${ACCESS_CHAIN}$`);
|
|
2365
|
+
var STOPS_REQUEST = /^(?:(?:return|throw|redirect|notFound)\b|error\s*\(\s*40[13]\b)/;
|
|
2366
|
+
var DENIED_STATUS = /\b(?:return|throw)\b[\s\S]{0,300}\bstatus(?:Code)?\s*[:(=]\s*(?:401|403)\b|\berror\s*\(\s*40[13]\b/i;
|
|
2323
2367
|
function closingDelimiter(source, start, open, close) {
|
|
2324
2368
|
let depth = 0;
|
|
2325
2369
|
for (let i = start; i < source.length; i++) {
|
|
@@ -2343,6 +2387,10 @@ function controlledStatement(source, afterCondition) {
|
|
|
2343
2387
|
const end = semicolon === -1 ? Math.min(source.length, start + 400) : Math.min(semicolon + 1, start + 400);
|
|
2344
2388
|
return source.slice(start, end);
|
|
2345
2389
|
}
|
|
2390
|
+
function rejectsMissingIdentity(term, returnsDeniedStatus) {
|
|
2391
|
+
const rejects = NEGATED_IDENTITY.test(term) || IDENTITY_IS_EMPTY.test(term) || IDENTITY_MISMATCH.test(term);
|
|
2392
|
+
return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus);
|
|
2393
|
+
}
|
|
2346
2394
|
function hasConditionalAuthGuard(code) {
|
|
2347
2395
|
const starts = /\bif\s*\(/g;
|
|
2348
2396
|
let match;
|
|
@@ -2356,7 +2404,7 @@ function hasConditionalAuthGuard(code) {
|
|
|
2356
2404
|
const pairs = delimiterPairs(body);
|
|
2357
2405
|
let stopsRequest = false;
|
|
2358
2406
|
for (let i = 0; i < body.length; i++) {
|
|
2359
|
-
if (
|
|
2407
|
+
if (STOPS_REQUEST.test(body.slice(i, i + 16)) && (i === 0 || !/[\w$]/.test(body[i - 1]))) {
|
|
2360
2408
|
stopsRequest = true;
|
|
2361
2409
|
break;
|
|
2362
2410
|
}
|
|
@@ -2368,19 +2416,55 @@ function hasConditionalAuthGuard(code) {
|
|
|
2368
2416
|
while (/\s/.test(code[statementStart] ?? "")) statementStart++;
|
|
2369
2417
|
starts.lastIndex = statementStart + statement.length;
|
|
2370
2418
|
if (!stopsRequest) continue;
|
|
2371
|
-
const returnsDeniedStatus =
|
|
2372
|
-
const negative =
|
|
2373
|
-
const term = part.trim();
|
|
2374
|
-
const rejects = /^!\s*[\w$.]+(?:\s*\([^=]*\))?$/.test(term) || /^[\w$.]+\s*={2,3}\s*(?:null|undefined|false)$/.test(term) || /^[\w$.]+\s*!={1,2}\s*(?!(?:null|undefined|false)\b)[\w$.]+$/.test(term);
|
|
2375
|
-
return rejects && (AUTH_CONDITION.test(term) || returnsDeniedStatus);
|
|
2376
|
-
});
|
|
2419
|
+
const returnsDeniedStatus = DENIED_STATUS.test(statement);
|
|
2420
|
+
const negative = !/&&|\?(?!\.)/.test(condition) && condition.split("||").some((part) => rejectsMissingIdentity(part.trim(), returnsDeniedStatus));
|
|
2377
2421
|
if (negative) return true;
|
|
2378
2422
|
}
|
|
2379
2423
|
return false;
|
|
2380
2424
|
}
|
|
2425
|
+
function containsConditionalAuthCheck(code) {
|
|
2426
|
+
const pairs = delimiterPairs(code);
|
|
2427
|
+
for (const match of code.matchAll(/\bif\s*\(/g)) {
|
|
2428
|
+
const open = match.index + match[0].length - 1;
|
|
2429
|
+
const close = pairs.get(open);
|
|
2430
|
+
if (close === void 0) continue;
|
|
2431
|
+
let start = close + 1;
|
|
2432
|
+
while (/\s/.test(code[start] ?? "")) start++;
|
|
2433
|
+
let bodyStart = start;
|
|
2434
|
+
let bodyEnd;
|
|
2435
|
+
if (code[start] === "{") {
|
|
2436
|
+
bodyStart = start + 1;
|
|
2437
|
+
bodyEnd = pairs.get(start) ?? Math.min(code.length, start + 600);
|
|
2438
|
+
} else {
|
|
2439
|
+
const semicolon = code.slice(start, start + 400).indexOf(";");
|
|
2440
|
+
bodyEnd = semicolon === -1 ? Math.min(code.length, start + 400) : start + semicolon + 1;
|
|
2441
|
+
}
|
|
2442
|
+
let stop = -1;
|
|
2443
|
+
for (let i = bodyStart; i < bodyEnd; i++) {
|
|
2444
|
+
const window = code.slice(i, i + 16);
|
|
2445
|
+
if (STOPS_REQUEST.test(window) && (i === 0 || !/[\w$]/.test(code[i - 1]))) {
|
|
2446
|
+
stop = i;
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
if (/^if\s*\(/.test(window)) break;
|
|
2450
|
+
const end = pairs.get(i);
|
|
2451
|
+
if (end !== void 0) i = end;
|
|
2452
|
+
}
|
|
2453
|
+
if (stop === -1) continue;
|
|
2454
|
+
const returnsDeniedStatus = DENIED_STATUS.test(code.slice(stop, stop + 400));
|
|
2455
|
+
const condition = code.slice(open + 1, close);
|
|
2456
|
+
if (condition.split(/\|\||&&/).some((part) => rejectsMissingIdentity(part.trim(), returnsDeniedStatus))) return true;
|
|
2457
|
+
}
|
|
2458
|
+
return false;
|
|
2459
|
+
}
|
|
2460
|
+
var authSignalCache = /* @__PURE__ */ new WeakMap();
|
|
2381
2461
|
function hasAuthSignal(file) {
|
|
2462
|
+
const cached = authSignalCache.get(file);
|
|
2463
|
+
if (cached !== void 0) return cached;
|
|
2382
2464
|
const code = noiseMaskedOf(file);
|
|
2383
|
-
|
|
2465
|
+
const found = AUTH_ENFORCING_CALL.test(code) || hasConditionalAuthGuard(code);
|
|
2466
|
+
authSignalCache.set(file, found);
|
|
2467
|
+
return found;
|
|
2384
2468
|
}
|
|
2385
2469
|
function delimiterPairs(code) {
|
|
2386
2470
|
const pairs = /* @__PURE__ */ new Map();
|
|
@@ -2508,7 +2592,7 @@ function unguardedOperations(file, ops) {
|
|
|
2508
2592
|
return true;
|
|
2509
2593
|
});
|
|
2510
2594
|
}
|
|
2511
|
-
var CLIENT_CONSTRUCTOR = /\b(?:createClient|createServerClient)\s*(?:<[^()]{0,200}
|
|
2595
|
+
var CLIENT_CONSTRUCTOR = /\b(?:createClient|createServerClient)\s*(?:<[^()]{0,200}>\s*)?\(/;
|
|
2512
2596
|
var SERVICE_ROLE_ENV = /\bSUPABASE_SERVICE_ROLE(?:_KEY)?\b|\bSERVICE_ROLE_KEY\b|\bSUPABASE_SECRET_KEY\b/;
|
|
2513
2597
|
var SERVICE_ROLE_LITERAL = new RegExp(String.raw`['"\`](${JWT_SOURCE}|${SB_SECRET_SOURCE})['"\`]`, "g");
|
|
2514
2598
|
var ENV_BRACKET_ACCESS = /(?:process\.env|import\.meta\.env)\s*\[\s*['"]([^'"]+)['"]\s*\]/g;
|
|
@@ -2521,8 +2605,11 @@ function referencesServiceRole(code, source) {
|
|
|
2521
2605
|
}
|
|
2522
2606
|
return false;
|
|
2523
2607
|
}
|
|
2608
|
+
var NUXT_SUPABASE_ADMIN = /\bserverSupabaseServiceRole\s*(?:<[^()]{0,200}>\s*)?\(/;
|
|
2609
|
+
var NUXT_SUPABASE_SESSION = /\bserverSupabaseClient\s*(?:<[^()]{0,200}>\s*)?\(/;
|
|
2524
2610
|
function buildsAdminClient(file) {
|
|
2525
2611
|
const code = noiseMaskedOf(file);
|
|
2612
|
+
if (NUXT_SUPABASE_ADMIN.test(code)) return true;
|
|
2526
2613
|
if (!CLIENT_CONSTRUCTOR.test(code)) return false;
|
|
2527
2614
|
const source = commentsMaskedOf(file);
|
|
2528
2615
|
if (referencesServiceRole(code, source)) return true;
|
|
@@ -2535,6 +2622,8 @@ function buildsAdminClient(file) {
|
|
|
2535
2622
|
}
|
|
2536
2623
|
function buildsSessionClient(file) {
|
|
2537
2624
|
const code = noiseMaskedOf(file);
|
|
2625
|
+
if (NUXT_SUPABASE_ADMIN.test(code)) return false;
|
|
2626
|
+
if (NUXT_SUPABASE_SESSION.test(code)) return true;
|
|
2538
2627
|
if (!CLIENT_CONSTRUCTOR.test(code)) return false;
|
|
2539
2628
|
if (referencesServiceRole(code, commentsMaskedOf(file))) return false;
|
|
2540
2629
|
return /\bcookies\b/.test(code);
|
|
@@ -2564,17 +2653,29 @@ function normalizeSpec(spec, fromPath) {
|
|
|
2564
2653
|
alias: false
|
|
2565
2654
|
};
|
|
2566
2655
|
}
|
|
2567
|
-
const
|
|
2656
|
+
const lib = /^\$lib\/(.+)$/.exec(spec);
|
|
2657
|
+
if (lib) return { key: moduleKey(`src/lib/${lib[1]}`), alias: true };
|
|
2658
|
+
const alias = /^(?:[@~#]|~~|@@)\/(.+)$/.exec(spec);
|
|
2568
2659
|
return alias ? { key: moduleKey(alias[1]), alias: true } : null;
|
|
2569
2660
|
}
|
|
2570
2661
|
function usesAdminClient(route, adminModules, allFiles) {
|
|
2571
|
-
return buildsAdminClient(route) || importsAnyOf(route, adminModules, allFiles);
|
|
2662
|
+
return buildsAdminClient(route.file) || importsAnyOf(route, adminModules, allFiles) || autoImportsAnyOf(route, adminModules);
|
|
2572
2663
|
}
|
|
2573
2664
|
function usesSessionClient(route, sessionModules, allFiles) {
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2665
|
+
if (route.framework === "sveltekit" && /\blocals\s*\.\s*supabase\b/.test(noiseMaskedOf(route.file))) return true;
|
|
2666
|
+
return buildsSessionClient(route.file) || importsAnyOf(route, sessionModules, allFiles) || autoImportsAnyOf(route, sessionModules);
|
|
2667
|
+
}
|
|
2668
|
+
function autoImportsAnyOf(route, modules) {
|
|
2669
|
+
if (route.framework !== "nuxt") return false;
|
|
2670
|
+
const utils = `${route.scope}server/utils/`;
|
|
2671
|
+
const code = noiseMaskedOf(route.file);
|
|
2672
|
+
return modules.some((module) => module.path.startsWith(utils) && exportedNames(module).some((name) => new RegExp(`(?<![\\w$.])${name.replace(/\$/g, "\\$")}\\s*\\(`).test(code)));
|
|
2673
|
+
}
|
|
2674
|
+
function exportedNames(file) {
|
|
2675
|
+
const names = noiseMaskedOf(file).matchAll(
|
|
2676
|
+
/\bexport\s+(?:async\s+)?(?:function\s*\*?\s*|const\s+|let\s+|var\s+)([A-Za-z_$][\w$]*)/g
|
|
2677
|
+
);
|
|
2678
|
+
return [...names].map((m) => m[1]);
|
|
2578
2679
|
}
|
|
2579
2680
|
function importedModules(file, allFiles, aliasScope) {
|
|
2580
2681
|
const found = [];
|
|
@@ -2593,7 +2694,8 @@ function importedModules(file, allFiles, aliasScope) {
|
|
|
2593
2694
|
const prefix = aliasScope;
|
|
2594
2695
|
for (const key of [
|
|
2595
2696
|
moduleKey(`${prefix}${target.key}`),
|
|
2596
|
-
moduleKey(`${prefix}src/${target.key}`)
|
|
2697
|
+
moduleKey(`${prefix}src/${target.key}`),
|
|
2698
|
+
moduleKey(`${prefix}app/${target.key}`)
|
|
2597
2699
|
]) {
|
|
2598
2700
|
found.push(...index.get(key) ?? []);
|
|
2599
2701
|
}
|
|
@@ -2605,8 +2707,8 @@ function importsAnyOf(route, modules, allFiles) {
|
|
|
2605
2707
|
if (modules.length === 0) return false;
|
|
2606
2708
|
const targetPaths = new Set(modules.map((file) => file.path));
|
|
2607
2709
|
const visited = /* @__PURE__ */ new Set();
|
|
2608
|
-
const aliasScope =
|
|
2609
|
-
const queue = importedModules(route, allFiles, aliasScope);
|
|
2710
|
+
const aliasScope = route.scope;
|
|
2711
|
+
const queue = importedModules(route.file, allFiles, aliasScope);
|
|
2610
2712
|
while (queue.length > 0) {
|
|
2611
2713
|
const file = queue.pop();
|
|
2612
2714
|
if (targetPaths.has(file.path)) return true;
|
|
@@ -2856,7 +2958,7 @@ function middlewareCovers(ctx, routePath, url) {
|
|
|
2856
2958
|
const mw = middlewareFor(ctx, routePath);
|
|
2857
2959
|
if (!mw) return false;
|
|
2858
2960
|
if (!hasAuthSignal(mw)) return false;
|
|
2859
|
-
const config =
|
|
2961
|
+
const { config, readable } = compiledMatchersOf(mw);
|
|
2860
2962
|
if (config.kind === "absent") return true;
|
|
2861
2963
|
if (config.kind === "unreadable") {
|
|
2862
2964
|
ctx.reportIncomplete(
|
|
@@ -2865,11 +2967,39 @@ function middlewareCovers(ctx, routePath, url) {
|
|
|
2865
2967
|
);
|
|
2866
2968
|
return true;
|
|
2867
2969
|
}
|
|
2868
|
-
const parsed = config.patterns.map(matcherToRegex);
|
|
2869
|
-
const readable = parsed.filter((re) => re !== null);
|
|
2870
2970
|
if (readable.length === 0) return true;
|
|
2871
2971
|
return readable.some((re) => re.test(url));
|
|
2872
2972
|
}
|
|
2973
|
+
var matcherCache = /* @__PURE__ */ new WeakMap();
|
|
2974
|
+
function compiledMatchersOf(file) {
|
|
2975
|
+
const cached = matcherCache.get(file);
|
|
2976
|
+
if (cached !== void 0) return cached;
|
|
2977
|
+
const config = extractMatcherConfig(file);
|
|
2978
|
+
const readable = config.kind === "patterns" ? config.patterns.map(matcherToRegex).filter((re) => re !== null) : [];
|
|
2979
|
+
const compiled = { config, readable };
|
|
2980
|
+
matcherCache.set(file, compiled);
|
|
2981
|
+
return compiled;
|
|
2982
|
+
}
|
|
2983
|
+
function globalGuardFor(ctx, route) {
|
|
2984
|
+
const relative3 = (file) => file.path.startsWith(route.scope) ? file.path.slice(route.scope.length) : null;
|
|
2985
|
+
const candidates = ctx.files.filter((file) => {
|
|
2986
|
+
const path = relative3(file);
|
|
2987
|
+
if (path === null) return false;
|
|
2988
|
+
if (route.framework === "sveltekit") return /^src\/hooks\.server\.[mc]?[jt]s$/.test(path);
|
|
2989
|
+
if (route.framework === "nuxt") return /^server\/middleware\/.+\.[mc]?[jt]s$/.test(path);
|
|
2990
|
+
return false;
|
|
2991
|
+
});
|
|
2992
|
+
return candidates.find(containsAuthCheck)?.path ?? null;
|
|
2993
|
+
}
|
|
2994
|
+
var authCheckCache = /* @__PURE__ */ new WeakMap();
|
|
2995
|
+
function containsAuthCheck(file) {
|
|
2996
|
+
const cached = authCheckCache.get(file);
|
|
2997
|
+
if (cached !== void 0) return cached;
|
|
2998
|
+
const code = noiseMaskedOf(file);
|
|
2999
|
+
const found = AUTH_ENFORCING_CALL.test(code) || containsConditionalAuthCheck(code);
|
|
3000
|
+
authCheckCache.set(file, found);
|
|
3001
|
+
return found;
|
|
3002
|
+
}
|
|
2873
3003
|
var SECRET_SHAPED = /['"`]([A-Za-z0-9_\-.]{32,})['"`]/g;
|
|
2874
3004
|
function excerptFor(file, line) {
|
|
2875
3005
|
const raw = (file.lines[line - 1] ?? "").trim();
|
|
@@ -2885,7 +3015,7 @@ var apiAuthRule = {
|
|
|
2885
3015
|
id: "api/db-access-without-auth",
|
|
2886
3016
|
severity: "P0",
|
|
2887
3017
|
check(ctx) {
|
|
2888
|
-
const routes = ctx.files.filter((
|
|
3018
|
+
const routes = ctx.files.map(routeOf).filter((route) => route !== null);
|
|
2889
3019
|
if (routes.length === 0) return [];
|
|
2890
3020
|
for (const middlewareFile of ctx.files.filter((f) => MIDDLEWARE_FILE.test(f.path))) {
|
|
2891
3021
|
const config = extractMatcherConfig(middlewareFile);
|
|
@@ -2901,26 +3031,31 @@ var apiAuthRule = {
|
|
|
2901
3031
|
const sessionModules = ctx.files.filter(buildsSessionClient);
|
|
2902
3032
|
const findings = [];
|
|
2903
3033
|
for (const route of routes) {
|
|
2904
|
-
const ops = unguardedOperations(route, findDataOps(route));
|
|
3034
|
+
const ops = unguardedOperations(route.file, findDataOps(route.file));
|
|
2905
3035
|
if (ops.length === 0) continue;
|
|
2906
|
-
const url =
|
|
2907
|
-
if (isAuthEndpoint(
|
|
2908
|
-
if (middlewareCovers(ctx, route.path, url)) continue;
|
|
3036
|
+
const url = route.url;
|
|
3037
|
+
if (isAuthEndpoint(route)) continue;
|
|
3038
|
+
if ((route.framework === "next" || route.framework === "astro") && middlewareCovers(ctx, route.file.path, url)) continue;
|
|
3039
|
+
const globalGuard = globalGuardFor(ctx, route);
|
|
3040
|
+
const globalNote = globalGuard === null ? [] : [
|
|
3041
|
+
`${globalGuard} contains an authentication check that may cover ${url}. Its path conditions are written in code, so the scan cannot tell which routes it applies to; this finding is reported at lower confidence. Confirm that check runs for ${url}.`
|
|
3042
|
+
];
|
|
2909
3043
|
const admin = usesAdminClient(route, adminModules, ctx.files);
|
|
2910
3044
|
const hit = ops.find((o) => o.writes) ?? ops[0];
|
|
2911
|
-
const line = lineNumberAt(lineStartsOf(route.content), hit.index);
|
|
2912
|
-
const excerpt = excerptFor(route, line);
|
|
3045
|
+
const line = lineNumberAt(lineStartsOf(route.file.content), hit.index);
|
|
3046
|
+
const excerpt = excerptFor(route.file, line);
|
|
2913
3047
|
if (admin) {
|
|
2914
3048
|
findings.push({
|
|
2915
3049
|
ruleId: "api/admin-db-access-without-auth",
|
|
2916
3050
|
severity: "P0",
|
|
2917
|
-
//
|
|
2918
|
-
confidence: "certain",
|
|
3051
|
+
// 管理员客户端缺少鉴权时使用确定置信度;可能受全局鉴权覆盖时降为疑似。
|
|
3052
|
+
confidence: globalGuard === null ? "certain" : "likely",
|
|
2919
3053
|
title: `Anyone can call ${url} and it queries your database as admin`,
|
|
2920
|
-
file: route.path,
|
|
3054
|
+
file: route.file.path,
|
|
2921
3055
|
line,
|
|
2922
3056
|
excerpt,
|
|
2923
3057
|
why: [
|
|
3058
|
+
...globalNote,
|
|
2924
3059
|
`This route references a recognized admin client. Such clients can bypass normal per-user access checks; verify the effective credentials and granted permissions.`,
|
|
2925
3060
|
`The scan did not recognize an authentication guard protecting this operation or applicable middleware. Indirect wrappers and deployment-level controls may not be recognized; verify them before exposing this route.`,
|
|
2926
3061
|
`If ${url} is reachable without authentication, callers can trigger the admin-backed operations implemented by this handler.`,
|
|
@@ -2945,10 +3080,11 @@ var apiAuthRule = {
|
|
|
2945
3080
|
// 公开写入可能是业务设计,保留疑似置信度。
|
|
2946
3081
|
confidence: "likely",
|
|
2947
3082
|
title: `${url} writes to your database with no sign-in check`,
|
|
2948
|
-
file: route.path,
|
|
3083
|
+
file: route.file.path,
|
|
2949
3084
|
line,
|
|
2950
3085
|
excerpt,
|
|
2951
3086
|
why: [
|
|
3087
|
+
...globalNote,
|
|
2952
3088
|
`This route changes data, and the scan did not recognize an authentication guard before the operation. Indirect guards and runtime controls require manual verification.`,
|
|
2953
3089
|
`If this route is publicly reachable and no other control rejects the caller, unauthenticated requests can trigger this write.`,
|
|
2954
3090
|
`If this is a public form \u2014 a waitlist, a contact box \u2014 that may be intentional. It is still worth rate limiting, because an open write endpoint is what gets a database filled with spam overnight.`
|
|
@@ -2992,7 +3128,7 @@ function headerExpression(content, start) {
|
|
|
2992
3128
|
return content.slice(start, end);
|
|
2993
3129
|
}
|
|
2994
3130
|
var CORS_ORIGIN_OPTION = /\borigin\s*:\s*/gi;
|
|
2995
|
-
var CORS_ORIGIN_PROPERTY_START = /\borigin\s*:\s*(?:async\s+)?(?:(function)\s*(?:[A-Za-z_$][\w$]
|
|
3131
|
+
var CORS_ORIGIN_PROPERTY_START = /\borigin\s*:\s*(?:async\s+)?(?:(function)\s*(?:[A-Za-z_$][\w$]*\s*)?)?\(/gi;
|
|
2996
3132
|
var CORS_ORIGIN_METHOD_START = /\borigin\s*\(/gi;
|
|
2997
3133
|
function closingParameterList(source, open) {
|
|
2998
3134
|
let depth = 0;
|
|
@@ -3114,8 +3250,14 @@ function callbackAnswer(params, body) {
|
|
|
3114
3250
|
var ORIGIN_IS_CHECKED = /\bincludes\s*\(|\bindexOf\s*\(|===|!==|==|!=|\.test\s*\(|\.some\s*\(|\bstartsWith\s*\(|\.match\s*\(|\.has\s*\(|\bif\b|\?|\ballow(?:ed|list)?\b|\bwhitelist\b/i;
|
|
3115
3251
|
var ACAC_HEADER = /['"]Access-Control-Allow-Credentials['"][\s\S]{0,40}?\btrue\b/gi;
|
|
3116
3252
|
var CORS_CREDENTIALS_OPTION = /\bcredentials\s*:\s*true\b/g;
|
|
3253
|
+
var TRAILING_PUNCTUATION = /[\s;,)}\]!]/;
|
|
3254
|
+
function trimTrailingPunctuation(text) {
|
|
3255
|
+
let end = text.length;
|
|
3256
|
+
while (end > 0 && TRAILING_PUNCTUATION.test(text[end - 1])) end--;
|
|
3257
|
+
return text.slice(0, end);
|
|
3258
|
+
}
|
|
3117
3259
|
function classifyOrigin(raw) {
|
|
3118
|
-
let v = raw.trim()
|
|
3260
|
+
let v = trimTrailingPunctuation(raw.trim());
|
|
3119
3261
|
const opening = v[0];
|
|
3120
3262
|
if (opening === '"' || opening === "'" || opening === "`") {
|
|
3121
3263
|
const close = v.indexOf(opening, 1);
|
|
@@ -3453,7 +3595,8 @@ async function scan(root, options = {}) {
|
|
|
3453
3595
|
const started = Date.now();
|
|
3454
3596
|
const gitExecutable = resolveGitExecutable(root);
|
|
3455
3597
|
const git2 = detectGitRepo(root, gitExecutable);
|
|
3456
|
-
const
|
|
3598
|
+
const honorIgnoreMarkers = options.honorIgnoreMarkers !== false;
|
|
3599
|
+
const { files, skipped, ignored, vendored } = collectFiles(root, git2 === "repo", gitExecutable, {}, honorIgnoreMarkers);
|
|
3457
3600
|
const findings = [];
|
|
3458
3601
|
const errors = [];
|
|
3459
3602
|
const fileRules = FILE_RULES.filter((rule2) => shouldRunRule(rule2.id, options.only ?? [], options.skip ?? []));
|
|
@@ -3488,10 +3631,8 @@ async function scan(root, options = {}) {
|
|
|
3488
3631
|
errors.push({ ruleId: rule2.id, file: null, message: messageOf(err), kind: "crashed" });
|
|
3489
3632
|
}
|
|
3490
3633
|
}
|
|
3491
|
-
const
|
|
3492
|
-
|
|
3493
|
-
files
|
|
3494
|
-
);
|
|
3634
|
+
const deduped = dedupe(downgradeExampleContext(findings, files));
|
|
3635
|
+
const { kept, ignored: ignoredFindings } = honorIgnoreMarkers ? suppressIgnoredLines(deduped, files) : { kept: deduped, ignored: [] };
|
|
3495
3636
|
const selected = applyRuleSelection(kept, options);
|
|
3496
3637
|
const counts = /* @__PURE__ */ new Map();
|
|
3497
3638
|
const bounded = sortFindings(selected.kept).filter((finding) => {
|
|
@@ -4511,15 +4652,15 @@ var RULE_CATALOG = [
|
|
|
4511
4652
|
"Admin database access without a recognized auth guard",
|
|
4512
4653
|
"P0",
|
|
4513
4654
|
"certain",
|
|
4514
|
-
"Next.js
|
|
4515
|
-
"Syntactic guards only; runtime authentication and business authorization are not verified."
|
|
4655
|
+
"Server routes in Next.js, SvelteKit, Nuxt, Remix/React Router, and Astro, including workspace applications.",
|
|
4656
|
+
"Syntactic guards only; runtime authentication and business authorization are not verified. Guards in SvelteKit hooks or Nuxt server middleware lower confidence."
|
|
4516
4657
|
),
|
|
4517
4658
|
rule(
|
|
4518
4659
|
"api/db-write-without-auth",
|
|
4519
4660
|
"Database write without a recognized auth guard",
|
|
4520
4661
|
"P1",
|
|
4521
4662
|
"likely",
|
|
4522
|
-
"Next.js
|
|
4663
|
+
"Server routes in Next.js, SvelteKit, Nuxt, Remix/React Router, and Astro.",
|
|
4523
4664
|
"Session-scoped clients and indirect guards may need manual review."
|
|
4524
4665
|
),
|
|
4525
4666
|
rule(
|
|
@@ -4626,7 +4767,7 @@ function renderRuleCatalog() {
|
|
|
4626
4767
|
}
|
|
4627
4768
|
|
|
4628
4769
|
// src/cli.ts
|
|
4629
|
-
var VERSION = true ? "0.3.
|
|
4770
|
+
var VERSION = true ? "0.3.2" : "0.0.0-dev";
|
|
4630
4771
|
function finish(code) {
|
|
4631
4772
|
process.exitCode = code;
|
|
4632
4773
|
}
|
|
@@ -4691,6 +4832,7 @@ function parseArgs(argv) {
|
|
|
4691
4832
|
skip: [],
|
|
4692
4833
|
sarif: null,
|
|
4693
4834
|
noConfig: false,
|
|
4835
|
+
noIgnoreMarkers: false,
|
|
4694
4836
|
help: false,
|
|
4695
4837
|
version: false,
|
|
4696
4838
|
listRules: false,
|
|
@@ -4759,6 +4901,9 @@ function parseArgs(argv) {
|
|
|
4759
4901
|
case "--no-config":
|
|
4760
4902
|
args.noConfig = true;
|
|
4761
4903
|
break;
|
|
4904
|
+
case "--no-ignore-markers":
|
|
4905
|
+
args.noIgnoreMarkers = true;
|
|
4906
|
+
break;
|
|
4762
4907
|
case "--list-rules":
|
|
4763
4908
|
args.listRules = true;
|
|
4764
4909
|
break;
|
|
@@ -4807,6 +4952,9 @@ var HELP = `
|
|
|
4807
4952
|
--sarif[=F] Write a SARIF 2.1.0 log for CI code scanning
|
|
4808
4953
|
(default canship.sarif)
|
|
4809
4954
|
--no-config Ignore canship.config.json in the scanned directory
|
|
4955
|
+
--no-ignore-markers
|
|
4956
|
+
Disregard canship-ignore-file and canship-ignore-next-line
|
|
4957
|
+
markers; use with --no-config for untrusted projects
|
|
4810
4958
|
--list-rules List rule IDs, scope, and limits; add --json for structured output
|
|
4811
4959
|
--no-excerpts Omit source excerpts from every report; paths and descriptions remain
|
|
4812
4960
|
-h, --help Show this help
|
|
@@ -4887,7 +5035,7 @@ async function main() {
|
|
|
4887
5035
|
const showAll = args.showAll || config.all === true;
|
|
4888
5036
|
const bestEffort = args.bestEffort;
|
|
4889
5037
|
const baselinePath = args.baseline !== null ? resolve2(args.baseline) : args.baselineDefault ? resolve2(args.root, DEFAULT_BASELINE_PATH) : config.baseline !== void 0 ? insideProject(args.root, config.baseline) : null;
|
|
4890
|
-
const scanned = await scan(args.root, { only, skip });
|
|
5038
|
+
const scanned = await scan(args.root, { only, skip, honorIgnoreMarkers: !args.noIgnoreMarkers });
|
|
4891
5039
|
if (args.baselineWrite !== null || args.baselineWriteDefault) {
|
|
4892
5040
|
const target = args.baselineWrite !== null ? resolve2(args.baselineWrite) : resolve2(args.root, DEFAULT_BASELINE_PATH);
|
|
4893
5041
|
const baseline = buildBaseline(scanned.findings);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "canship",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Static scanner for exposed credentials and open access rules in JS/TS front-end projects. Scans local files without executing project code or making network requests.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|