canship 0.1.0 → 0.2.0
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.md +204 -114
- package/README.zh-CN.md +206 -116
- package/dist/cli.js +1180 -173
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,114 +1,204 @@
|
|
|
1
|
-
# canship
|
|
2
|
-
|
|
3
|
-
canship is a read-only static scanner for JavaScript and TypeScript web projects. It detects exposed credentials,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
|
|
|
22
|
-
|
|
|
23
|
-
| Supabase
|
|
24
|
-
|
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
|
45
|
-
|
|
46
|
-
| `--
|
|
47
|
-
| `--
|
|
48
|
-
| `--
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
|
57
|
-
|
|
58
|
-
| `
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
1
|
+
# canship
|
|
2
|
+
|
|
3
|
+
canship is a read-only static scanner for JavaScript and TypeScript web projects. It detects exposed credentials, private values carried into browser-delivered code by a public environment prefix (Next.js, Vite, Nuxt, Create React App, Expo, Gatsby, Vue CLI, SvelteKit), Supabase tables without Row Level Security, Firebase rules left open, and API routes that reach data without checking who is calling.
|
|
4
|
+
|
|
5
|
+
That last check covers Next.js only — handlers under `app/api/**` and `pages/api/**`. A SvelteKit `+server.ts` or a Nuxt `server/api/` handler is scanned for everything else and is not checked for a missing authentication check. Every other check is framework-independent.
|
|
6
|
+
|
|
7
|
+
The scan does not execute project code, upload content, or initiate network requests. The npm package has no runtime dependencies. When scanning a Git repository, canship reads only the local working tree and local commit history.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx canship
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Node.js 18 or later is required. Git is optional; only commit-history checks require a local Git installation and a readable repository. If canship is not already in the npm cache, `npx` may download it from the npm registry before the scan starts; that download is performed by npm and is not part of the scan.
|
|
14
|
+
|
|
15
|
+
[简体中文](./README.zh-CN.md)
|
|
16
|
+
|
|
17
|
+
## Checks
|
|
18
|
+
|
|
19
|
+
| Check | Risk | Severity |
|
|
20
|
+
|---|---|---|
|
|
21
|
+
| Hardcoded credentials | A scanned file contains a recognised OpenAI, Anthropic, AWS, Stripe, GitHub, npm, Slack, SendGrid, private-key, or database credential | P0 |
|
|
22
|
+
| Private values in public environment variables | The value may be included in browser-delivered code | P0 |
|
|
23
|
+
| Supabase `service_role` key reachable from client code | A `service_role` key can bypass Row Level Security (RLS) policies | P0 |
|
|
24
|
+
| Git-tracked `.env` files | Recognised credentials remain in repository history; substantial unrecognised values may also produce a lower-confidence finding | P0 |
|
|
25
|
+
| Supabase tables without RLS | A table exposed through the Supabase Data API lacks row-level access control | P1 |
|
|
26
|
+
| Firebase rules allowing unconditional access | Unauthorised clients may be able to read or write data | P1 |
|
|
27
|
+
| Next.js API routes accessing data without authentication | An unverified caller may access data or perform administrative operations | P0 / P1 |
|
|
28
|
+
| CORS reflecting `Origin` while allowing credentials | Another site may access an endpoint with the user's credentials and read the response | P1 |
|
|
29
|
+
|
|
30
|
+
Severity (P0, P1, P2) describes potential impact. Confidence (`certain`, `likely`) describes how strongly the code supports the finding. A `certain` P0/P1 finding blocks release and exits with status `1`; all other findings exit with status `2`.
|
|
31
|
+
|
|
32
|
+
`Access-Control-Allow-Origin: *` combined with credentials is reported at P2 because browsers reject that configuration. A bare wildcard is not reported.
|
|
33
|
+
|
|
34
|
+
For Git-tracked `.env` files, a recognised credential is reported at `certain` confidence. A substantial value that is not recognised but does not look public or placeholder-like is reported at `likely` confidence. Environment templates, public values, placeholders, and short settings do not trigger this Git rule solely because the file is tracked.
|
|
35
|
+
|
|
36
|
+
## Usage
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
npx canship [path]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The current directory is scanned when no path is provided.
|
|
43
|
+
|
|
44
|
+
| Option | Description |
|
|
45
|
+
|---|---|
|
|
46
|
+
| `-a`, `--all` | Show `likely` findings |
|
|
47
|
+
| `--json` | Write machine-readable JSON |
|
|
48
|
+
| `--fix-prompt` | Write remediation instructions for a coding assistant |
|
|
49
|
+
| `--report[=file]` | Write a self-contained HTML report; defaults to `canship-report.html` |
|
|
50
|
+
| `--best-effort` | Allow exit `0` when the scan is incomplete and has no findings; does not change the status of existing findings |
|
|
51
|
+
| `--baseline[=file]` | Hide findings recorded in the baseline, so only new ones are reported; defaults to `canship-baseline.json` |
|
|
52
|
+
| `--baseline-write[=file]` | Record the current findings as a baseline and exit; defaults to `canship-baseline.json` |
|
|
53
|
+
| `--only=ids` | Report only these rules; comma-separated and repeatable |
|
|
54
|
+
| `--skip=ids` | Report everything except these rules; comma-separated and repeatable |
|
|
55
|
+
| `--sarif[=file]` | Write a SARIF 2.1.0 log for CI code scanning; defaults to `canship.sarif` |
|
|
56
|
+
| `--no-config` | Ignore `canship.config.json` in the scanned directory |
|
|
57
|
+
| `-h`, `--help` | Show help |
|
|
58
|
+
| `-v`, `--version` | Show the version |
|
|
59
|
+
|
|
60
|
+
`--json` and `--fix-prompt` are mutually exclusive. `--report` writes a separate file and may be combined with either mode.
|
|
61
|
+
|
|
62
|
+
### Exit status
|
|
63
|
+
|
|
64
|
+
| Status | Meaning |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `0` | No findings at any confidence and the scan completed; with `--best-effort`, it may also mean an incomplete scan was accepted |
|
|
67
|
+
| `1` | At least one `certain` P0/P1 finding |
|
|
68
|
+
| `2` | Findings exist, but none is a confirmed P0/P1 release blocker |
|
|
69
|
+
| `3` | Invalid arguments, a tool error, or an incomplete scan without `--best-effort` |
|
|
70
|
+
|
|
71
|
+
When findings and an incomplete scan coexist, status `1` or `2` takes precedence. The JSON fields `partial`, `errors`, and `skipped` still preserve the incomplete-scan state.
|
|
72
|
+
|
|
73
|
+
The default view expands only `certain` findings. Hidden `likely` findings still produce status `2`; the terminal and HTML report display a warning, and JSON reports the count in `hiddenLikely`. Use `--all` to include their full details.
|
|
74
|
+
|
|
75
|
+
### Excluding a file
|
|
76
|
+
|
|
77
|
+
Add `canship-ignore-file` on a line by itself to exclude the entire file. The marker may be wrapped only in `//`, `#`, `--`, `*`, `/* */`, or `<!-- -->` comment syntax. Intentionally excluded files are listed in the report and do not make the scan incomplete.
|
|
78
|
+
|
|
79
|
+
### Excluding a single line
|
|
80
|
+
|
|
81
|
+
Add `canship-ignore-next-line` on the line above a finding to suppress it there. The same comment syntax applies, and the marker must be the whole content of its line — a line that also holds code or prose does not suppress anything.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// canship-ignore-next-line
|
|
85
|
+
const documentedExample = "sk-proj-not-a-real-key"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
A bare marker suppresses every rule on the following line. A rule id after it narrows the suppression to that rule, so a line with one known false positive is not also blind to a different finding:
|
|
89
|
+
|
|
90
|
+
```ts
|
|
91
|
+
// canship-ignore-next-line secrets/hardcoded/openai
|
|
92
|
+
const key = process.env.OPENAI_KEY
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Rule ids appear in `--json` output; the terminal and HTML reports do not print them, which is why the bare form exists.
|
|
96
|
+
|
|
97
|
+
The marker governs the line immediately after it, with no allowance for blank lines. Suppressed findings are listed by file, line, and rule in the terminal, the HTML report, and the `ignoredFindings` field of `--json`. They do not make the scan incomplete, and the report states that the result is not finding-free.
|
|
98
|
+
|
|
99
|
+
### Configuration
|
|
100
|
+
|
|
101
|
+
Settings a project makes once can be committed to `canship.config.json` in the scanned directory. A command-line flag always overrides the file.
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{
|
|
105
|
+
"baseline": "canship-baseline.json",
|
|
106
|
+
"skip": ["cors/wildcard-with-credentials"],
|
|
107
|
+
"all": false
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
| Setting | Equivalent flag |
|
|
112
|
+
|---|---|
|
|
113
|
+
| `baseline` | `--baseline=file` |
|
|
114
|
+
| `only` | `--only=ids` |
|
|
115
|
+
| `skip` | `--skip=ids` |
|
|
116
|
+
| `all` | `--all` |
|
|
117
|
+
|
|
118
|
+
The format is JSON and not JavaScript. A `canship.config.js` would be project code, and the scan does not execute project code.
|
|
119
|
+
|
|
120
|
+
An unknown setting, a wrong type, a rule id that names no rule, or `only` and `skip` together are all errors and exit `3`. A missing config file is not an error. A `baseline` path must stay inside the scanned project; the `--baseline` flag is not restricted that way.
|
|
121
|
+
|
|
122
|
+
**The config file comes out of the directory being scanned.** When that directory is code you control, this is the point of the feature. When it is not — a dependency, a fork, an unreviewed pull request — the project can use it to switch off the rules that would report it. `--no-config` ignores the file entirely.
|
|
123
|
+
|
|
124
|
+
`bestEffort` is deliberately not a setting. It turns an incomplete scan from exit `3` into exit `0`, and that is a judgement for whoever runs canship rather than a property of the project being scanned. Naming it in the file is an error rather than something quietly ignored. Use `--best-effort`.
|
|
125
|
+
|
|
126
|
+
### Selecting rules
|
|
127
|
+
|
|
128
|
+
`--only` and `--skip` take the rule ids that appear in `--json` output, comma-separated and repeatable. A selector matches an id exactly, or matches every id beneath it at a `/` boundary — `secrets` covers every credential format, `secrets/hardcoded/openai` covers one. A partial id such as `secrets/hardcoded/open` matches nothing and is rejected, so a mistyped id cannot quietly disable a rule.
|
|
129
|
+
|
|
130
|
+
`--only` and `--skip` cannot be combined. Selection filters findings rather than skipping the rules themselves, so it does not reduce scan time. Every report states which selection was in force and how many findings it hid.
|
|
131
|
+
|
|
132
|
+
### Baseline
|
|
133
|
+
|
|
134
|
+
An existing project usually has findings on the first run. A baseline records them so that subsequent runs report only what appeared afterwards, which is what makes canship usable in continuous integration for a project that did not start with it.
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npx canship --baseline-write # accept the current findings
|
|
138
|
+
npx canship --baseline # report only new ones
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
`--baseline-write` writes `canship-baseline.json` and exits `0` without scanning further. Commit that file: it is the record of what was accepted, and it is meant to be reviewed in the pull request that adds it.
|
|
142
|
+
|
|
143
|
+
**Consider what committing it publishes.** Each entry names a file path, a rule id, and a finding title, and every entry describes a problem that has not been fixed. canship searches gitignored credential files by design, so an entry may describe a file the repository does not contain — `.env.local` and the name of a variable in it, for example. The file holds no credential values. On a private repository this is the intended use; on a public one, weigh the disclosure before committing.
|
|
144
|
+
|
|
145
|
+
Paths are resolved where they came from: a path typed on the command line is relative to the working directory, a bare `--baseline` or `--baseline-write` uses the scanned project's own `canship-baseline.json`, and a path in `canship.config.json` is relative to that file. So `npx canship ./app --baseline-write` writes into `./app`, which is where `npx canship ./app --baseline` then looks.
|
|
146
|
+
|
|
147
|
+
A baseline entry is matched by rule, file, title, and a digest of its original source evidence; only findings without locatable evidence fall back to the excerpt. The source digest is computed before redaction and truncation, so replacing a credential cannot hide behind the old display text. The line number is deliberately excluded, so editing a file above a line-based finding does not report it as new. Each entry carries the number of times it was seen; an additional occurrence beyond that count is reported.
|
|
148
|
+
|
|
149
|
+
The baseline format is now version 2, and the SARIF fingerprint is named `canshipFindingV2`. Older baselines are rejected with exit code `3`, never automatically converted or overwritten. Review the findings again before regenerating with `--baseline-write`.
|
|
150
|
+
|
|
151
|
+
The baseline stores a SHA-256 hash rather than the excerpt itself, because the file is intended to be committed and canship cannot guarantee that an unrecognised credential is masked.
|
|
152
|
+
|
|
153
|
+
A baseline hides real findings. Every output states how many:
|
|
154
|
+
|
|
155
|
+
- the terminal and HTML report never show a clean result while a baseline is suppressing findings, and name the count and the file
|
|
156
|
+
- `--json` reports `baselineSuppressed`
|
|
157
|
+
- baseline entries that no longer match anything are reported as `baselineStale`; they do not affect the exit status
|
|
158
|
+
|
|
159
|
+
Findings of every confidence are recorded. A baseline that cannot be read — missing, malformed, or written by a different format version — exits `3` rather than proceeding with no suppression. `--baseline` and `--baseline-write` cannot be combined.
|
|
160
|
+
|
|
161
|
+
### Remediation instructions
|
|
162
|
+
|
|
163
|
+
`--fix-prompt` separates code changes that a coding assistant can perform from actions that require the project maintainer, such as rotating credentials, changing provider settings, or rewriting Git history.
|
|
164
|
+
|
|
165
|
+
## Rule activation
|
|
166
|
+
|
|
167
|
+
| Rule | Activation conditions |
|
|
168
|
+
|---|---|
|
|
169
|
+
| Public environment variables | The name starts with `NEXT_PUBLIC_`, `VITE_`, `REACT_APP_`, `EXPO_PUBLIC_`, `NUXT_PUBLIC_`, `GATSBY_`, `VUE_APP_`, or `PUBLIC_` |
|
|
170
|
+
| Git-tracked environment files | The target is inside a readable local Git repository whose `.git` metadata is inside the checkout or names it back; templates, public values, placeholders, and short settings are excluded, and at most the 100 most recent relevant revisions of each file are inspected |
|
|
171
|
+
| Next.js API authentication | Only data-accessing handlers under `app/api/**` and `pages/api/**`; recognises enforcing authentication calls, identity conditions that control rejection, and middleware `matcher` coverage for the route |
|
|
172
|
+
| Supabase RLS | The current project scope contains `supabase/`, an `@supabase/supabase-js` or `@supabase/ssr` import, or a `SUPABASE_URL`; table-related DDL is replayed across migrations within that project scope |
|
|
173
|
+
| Other credential and configuration rules | Match known file formats and content patterns without requiring a specific front-end framework |
|
|
174
|
+
|
|
175
|
+
## Known limitations
|
|
176
|
+
|
|
177
|
+
- canship uses static heuristics and does not verify runtime behaviour. Custom authentication wrappers, dynamic configuration, and unsupported syntax can cause false positives or false negatives.
|
|
178
|
+
- Detection and redaction use the same credential patterns. A credential that canship cannot recognise cannot be guaranteed to be masked; if another rule quotes the same line, the original value may appear in the report. Treat reports as internal material.
|
|
179
|
+
- Entropy-based detection is intentionally not used because random identifiers, hashes, and ordinary Base64 text cannot be classified reliably from entropy alone.
|
|
180
|
+
- Files are limited to 2 MiB, traversal to 16 directory levels, and output to 100 findings per file. Git history checks inspect at most the 100 most recent relevant revisions of each file. Reaching a limit is recorded explicitly.
|
|
181
|
+
- Symbolic links are not followed and make the scan incomplete.
|
|
182
|
+
- Nested Git repositories and submodules are not expanded by the parent scan. They are listed as skipped and the parent scan is marked incomplete.
|
|
183
|
+
- A `.git` file may point outside the checkout. canship follows it only when the target names this checkout back, which is what git records for a linked worktree and for a submodule. Anything else is read as a redirect to an unrelated repository: file scanning continues, history checks are marked incomplete.
|
|
184
|
+
- Google, Firebase, and Maps `AIza...` keys are treated as public identifiers. Their application and API restrictions exist in Google Cloud and cannot be verified from local source, so the key value alone is not reported as a credential leak.
|
|
185
|
+
- Rate limiting, injection, dependency vulnerabilities, and business authorisation beyond caller authentication are outside the scan scope.
|
|
186
|
+
|
|
187
|
+
A canship result describes only what the implemented rules observed in the files that were read. It is not proof that the project has no other security defects.
|
|
188
|
+
|
|
189
|
+
## Planned
|
|
190
|
+
|
|
191
|
+
`--probe` is not implemented. The planned mode would, after explicit confirmation, issue read-only verification requests to service endpoints found in local project configuration. The current release does not initiate network requests.
|
|
192
|
+
|
|
193
|
+
## Contributing
|
|
194
|
+
|
|
195
|
+
Every new or changed detection rule should include at least two fixtures: one that must be reported and one that must not. See [`test/fixtures/`](./test/fixtures/).
|
|
196
|
+
|
|
197
|
+
```bash
|
|
198
|
+
npm ci
|
|
199
|
+
npm run prepublishOnly
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## License
|
|
203
|
+
|
|
204
|
+
[MIT](./LICENSE)
|