cleartoship 0.4.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/LICENSE +21 -0
- package/README.md +264 -0
- package/action.yml +154 -0
- package/dist/banner.d.ts +1 -0
- package/dist/banner.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +103 -0
- package/dist/data/popular.d.ts +17 -0
- package/dist/data/popular.js +103 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +4 -0
- package/dist/report.d.ts +15 -0
- package/dist/report.js +317 -0
- package/dist/scan.d.ts +24 -0
- package/dist/scan.js +81 -0
- package/dist/scanners/community.d.ts +2 -0
- package/dist/scanners/community.js +155 -0
- package/dist/scanners/dependencies.d.ts +21 -0
- package/dist/scanners/dependencies.js +549 -0
- package/dist/scanners/index.d.ts +8 -0
- package/dist/scanners/index.js +13 -0
- package/dist/scanners/rls.d.ts +7 -0
- package/dist/scanners/rls.js +531 -0
- package/dist/scanners/secrets.d.ts +2 -0
- package/dist/scanners/secrets.js +439 -0
- package/dist/scanners/server-actions.d.ts +2 -0
- package/dist/scanners/server-actions.js +466 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +10 -0
- package/dist/utils/ast.d.ts +13 -0
- package/dist/utils/ast.js +107 -0
- package/dist/utils/detect.d.ts +2 -0
- package/dist/utils/detect.js +77 -0
- package/dist/utils/entropy.d.ts +7 -0
- package/dist/utils/entropy.js +19 -0
- package/dist/utils/files.d.ts +13 -0
- package/dist/utils/files.js +154 -0
- package/dist/utils/osv.d.ts +26 -0
- package/dist/utils/osv.js +123 -0
- package/dist/utils/registry.d.ts +44 -0
- package/dist/utils/registry.js +195 -0
- package/dist/utils/sql.d.ts +20 -0
- package/dist/utils/sql.js +157 -0
- package/dist/utils/suppress.d.ts +19 -0
- package/dist/utils/suppress.js +56 -0
- package/dist/utils/traverse.d.ts +2 -0
- package/dist/utils/traverse.js +14 -0
- package/dist/vendor/gitleaks/rules.d.ts +13 -0
- package/dist/vendor/gitleaks/rules.js +1571 -0
- package/dist/vendor/guardvibe/index.d.ts +11 -0
- package/dist/vendor/guardvibe/index.js +60 -0
- package/dist/vendor/guardvibe/rules/advanced-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/advanced-security.js +418 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-host-security.js +190 -0
- package/dist/vendor/guardvibe/rules/ai-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-security.js +413 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/ai-tool-runtime.js +194 -0
- package/dist/vendor/guardvibe/rules/api-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/api-security.js +154 -0
- package/dist/vendor/guardvibe/rules/auth.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/auth.js +222 -0
- package/dist/vendor/guardvibe/rules/cicd.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cicd.js +108 -0
- package/dist/vendor/guardvibe/rules/core.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/core.js +484 -0
- package/dist/vendor/guardvibe/rules/cve-versions.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/cve-versions.js +1130 -0
- package/dist/vendor/guardvibe/rules/database.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/database.js +173 -0
- package/dist/vendor/guardvibe/rules/deployment.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/deployment.js +269 -0
- package/dist/vendor/guardvibe/rules/dockerfile.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/dockerfile.js +99 -0
- package/dist/vendor/guardvibe/rules/firebase.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/firebase.js +96 -0
- package/dist/vendor/guardvibe/rules/go.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/go.js +85 -0
- package/dist/vendor/guardvibe/rules/modern-stack.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/modern-stack.js +631 -0
- package/dist/vendor/guardvibe/rules/nextjs.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/nextjs.js +233 -0
- package/dist/vendor/guardvibe/rules/other-services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/other-services.js +72 -0
- package/dist/vendor/guardvibe/rules/payments.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/payments.js +123 -0
- package/dist/vendor/guardvibe/rules/react-native.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/react-native.js +132 -0
- package/dist/vendor/guardvibe/rules/services.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/services.js +149 -0
- package/dist/vendor/guardvibe/rules/shell.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/shell.js +72 -0
- package/dist/vendor/guardvibe/rules/sql.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/sql.js +60 -0
- package/dist/vendor/guardvibe/rules/supply-chain.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/supply-chain.js +243 -0
- package/dist/vendor/guardvibe/rules/terraform.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/terraform.js +84 -0
- package/dist/vendor/guardvibe/rules/types.d.ts +14 -0
- package/dist/vendor/guardvibe/rules/types.js +11 -0
- package/dist/vendor/guardvibe/rules/web-security.d.ts +2 -0
- package/dist/vendor/guardvibe/rules/web-security.js +258 -0
- package/examples/security.yml +23 -0
- package/package.json +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ClearToShip
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
# ClearToShip
|
|
2
|
+
|
|
3
|
+
> The 30-second pre-launch security clearance for AI-built & vibe-coded apps.
|
|
4
|
+
|
|
5
|
+
AI coding assistants write code fast, but they optimise for *"runs without errors"*, not
|
|
6
|
+
*"runs without leaks"*. The failure mode is almost never a dangerous line of code — it's an
|
|
7
|
+
**absent** one: the session check that was never written, the RLS policy that was never
|
|
8
|
+
enabled, the package name the model invented.
|
|
9
|
+
|
|
10
|
+
`cleartoship` is a static pre-flight check for exactly those gaps. No database connection, no
|
|
11
|
+
account, no upload — it reads your repo and exits non-zero if you shouldn't deploy.
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npx cleartoship
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## What it checks
|
|
18
|
+
|
|
19
|
+
**Next.js server surface** — Server Actions, Route Handlers, client boundary
|
|
20
|
+
|
|
21
|
+
| Rule | Severity | What it catches |
|
|
22
|
+
| --- | --- | --- |
|
|
23
|
+
| **CTS001** | critical | Server Action / Route Handler mutates the database with no session check |
|
|
24
|
+
| **CTS002** | high | Action takes caller input and writes it with no runtime schema validation |
|
|
25
|
+
| **CTS003** | critical | `SUPABASE_SERVICE_ROLE_KEY` client built inside a user-reachable action |
|
|
26
|
+
| **CTS004** | medium | Authenticated mutation keyed only on a caller-supplied id (IDOR) |
|
|
27
|
+
| **CTS040** | high | Client component reads a server-side `process.env` variable |
|
|
28
|
+
| **CTS041** | high | `supabase.auth.getSession()` used as a server-side auth check — it does not revalidate the JWT |
|
|
29
|
+
| **CTS042** | critical | Webhook endpoint accepts an unsigned, unverified payload |
|
|
30
|
+
| **CTS043** | high | Request body spread straight into a database write (mass assignment) |
|
|
31
|
+
| **CTS044** | medium | `.passthrough()` / `z.any()` makes the schema decorative |
|
|
32
|
+
| **CTS045** | critical | AI SDK client set to `dangerouslyAllowBrowser: true` |
|
|
33
|
+
| **CTS046** | high | Cron route with neither `CRON_SECRET` nor a session check |
|
|
34
|
+
|
|
35
|
+
**Supabase / PostgreSQL** — schema, RLS, storage
|
|
36
|
+
|
|
37
|
+
| Rule | Severity | What it catches |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| **CTS010** | critical | Public table with Row Level Security never enabled |
|
|
40
|
+
| **CTS011** | low | RLS on with no policies — fail-closed, but the feature is probably broken |
|
|
41
|
+
| **CTS012** | critical | Policy grants writes with an always-true predicate |
|
|
42
|
+
| **CTS013** | high | Anonymous `SELECT` over a table holding emails, tokens or billing ids |
|
|
43
|
+
| **CTS014** | high | Table has a `user_id` column but no policy compares it to `auth.uid()` |
|
|
44
|
+
| **CTS015** | medium | `SECURITY DEFINER` function without a pinned `search_path` |
|
|
45
|
+
| **CTS016** | medium/high | View with definer rights, or a materialized view, exposed over the Data API |
|
|
46
|
+
| **CTS017** | critical | `GRANT INSERT/UPDATE/DELETE … TO anon` |
|
|
47
|
+
| **CTS018** | critical | Policy trusts `user_metadata`, which the user can edit themselves |
|
|
48
|
+
| **CTS019** | critical | `auth.users` republished through a view in the public schema |
|
|
49
|
+
| **CTS050** | medium | Overlapping permissive policies — they OR together and only widen access |
|
|
50
|
+
| **CTS051** | high | Storage policy lets anyone list every object in every bucket |
|
|
51
|
+
| **CTS052** | high | `SECURITY DEFINER` function executable by `anon` |
|
|
52
|
+
|
|
53
|
+
**Supply chain** — hallucinated and hostile dependencies
|
|
54
|
+
|
|
55
|
+
| Rule | Severity | What it catches |
|
|
56
|
+
| --- | --- | --- |
|
|
57
|
+
| **CTS020** | critical | Dependency that **does not exist** on npm/PyPI — a hallucinated import |
|
|
58
|
+
| **CTS021** | high | Dependency registered days ago with near-zero downloads (slopsquat shape) |
|
|
59
|
+
| **CTS022** | low | Runtime dependency with almost no users |
|
|
60
|
+
| **CTS023** | high/medium | Name is one edit from a popular package (`expres` → `express`) |
|
|
61
|
+
| **CTS024** | by CVSS | Dependency version has a **published advisory**, resolved live from [OSV.dev](https://osv.dev) |
|
|
62
|
+
| **CTS025** | low | Dependency deprecated upstream |
|
|
63
|
+
| **CTS026** | critical | Registry serves HTTP 451 — the package was pulled for malware |
|
|
64
|
+
| **CTS027** | critical | Package was unpublished but still has installs; the name is open to takeover |
|
|
65
|
+
| **CTS028** | critical | `postinstall` hook that curls, evals or shells out |
|
|
66
|
+
|
|
67
|
+
Dependency rules read `package.json`, `requirements.txt` and `pyproject.toml` — **and**
|
|
68
|
+
`README.md`, `AGENTS.md`, `CLAUDE.md` and `.cursorrules`, because a hallucinated
|
|
69
|
+
`npm install` line gets copy-pasted out of an agent instruction file long before it
|
|
70
|
+
reaches a manifest.
|
|
71
|
+
|
|
72
|
+
**Secrets & client bundle**
|
|
73
|
+
|
|
74
|
+
| Rule | Severity | What it catches |
|
|
75
|
+
| --- | --- | --- |
|
|
76
|
+
| **CTS030** | critical | Hardcoded provider key — Supabase service-role, Stripe live, OpenAI, AWS, GitHub … |
|
|
77
|
+
| **CTS031** | critical | Server secret routed through a `NEXT_PUBLIC_` variable |
|
|
78
|
+
| **CTS032** | high | `.env` in a git repo with no matching `.gitignore` rule |
|
|
79
|
+
| **CTS033** | critical | `'use client'` component reaching for a server-only secret |
|
|
80
|
+
| **GL-\*** | high/critical | 219 further credential providers, vendored from [gitleaks](https://github.com/gitleaks/gitleaks) (MIT), gated on Shannon entropy |
|
|
81
|
+
|
|
82
|
+
**Community ruleset** — 436 additional rules vendored from
|
|
83
|
+
[GuardVibe](https://github.com/goklab/guardvibe) (Apache-2.0)
|
|
84
|
+
|
|
85
|
+
Reported under their upstream `VG###` ids. These cover ground the AST scanners
|
|
86
|
+
don't: known-vulnerable framework versions (`next` 14.2.3 is still shipped by a
|
|
87
|
+
lot of AI scaffolds and carries CVE-2025-29927, a middleware auth bypass),
|
|
88
|
+
Dockerfiles, Terraform, GitHub Actions pinning, prompt injection and MCP tool
|
|
89
|
+
runtimes, React Native, Go and shell.
|
|
90
|
+
|
|
91
|
+
27 upstream rules are **superseded** where ClearToShip's own AST check is more
|
|
92
|
+
precise, and 5 are **withheld** as measurably noisy — both lists carry a reason
|
|
93
|
+
per rule in `src/scanners/community.ts`. Run `--no-community` to use only
|
|
94
|
+
ClearToShip's rules. See [ATTRIBUTION.md](ATTRIBUTION.md).
|
|
95
|
+
|
|
96
|
+
Findings map to **OWASP Top 10:2025** and CWE.
|
|
97
|
+
|
|
98
|
+
## Usage
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
npx cleartoship # scan the whole project
|
|
102
|
+
npx cleartoship app supabase # scan specific paths
|
|
103
|
+
npx cleartoship --offline # no registry lookups
|
|
104
|
+
npx cleartoship --fix-prompt # prompt to paste into Cursor / Claude Code
|
|
105
|
+
npx cleartoship --json -o report.json # machine-readable
|
|
106
|
+
npx cleartoship --sarif -o results.sarif # GitHub code scanning
|
|
107
|
+
npx cleartoship --fail-on high # stricter CI gate (default: critical)
|
|
108
|
+
npx cleartoship --ignore CTS004,CTS022 # skip rules
|
|
109
|
+
npx cleartoship --no-community # ClearToShip rules only
|
|
110
|
+
npx cleartoship --markdown # markdown report (PR comments / summaries)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Suppress a single finding inline:
|
|
114
|
+
|
|
115
|
+
```ts
|
|
116
|
+
// cleartoship-ignore CTS001 — invoked only by a cron job, never by a request
|
|
117
|
+
export async function reconcileBilling() { … }
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`// cts-ignore` on its own suppresses every rule at that location. The directive
|
|
121
|
+
may sit at the top of a multi-line comment block, so a real justification has
|
|
122
|
+
room to be written out.
|
|
123
|
+
|
|
124
|
+
## Continuous integration
|
|
125
|
+
|
|
126
|
+
### GitHub Action (recommended)
|
|
127
|
+
|
|
128
|
+
Posts a summary comment on every pull request, blocks the merge on critical
|
|
129
|
+
findings, and optionally uploads to the Security tab. Copy
|
|
130
|
+
[`examples/security.yml`](examples/security.yml) into `.github/workflows/`:
|
|
131
|
+
|
|
132
|
+
```yaml
|
|
133
|
+
name: ClearToShip
|
|
134
|
+
on: [pull_request]
|
|
135
|
+
permissions:
|
|
136
|
+
contents: read
|
|
137
|
+
pull-requests: write
|
|
138
|
+
jobs:
|
|
139
|
+
preflight:
|
|
140
|
+
runs-on: ubuntu-latest
|
|
141
|
+
steps:
|
|
142
|
+
- uses: actions/checkout@v4
|
|
143
|
+
- uses: murtazaozdemir/cleartoship@v0.3.0
|
|
144
|
+
with:
|
|
145
|
+
fail-on: critical
|
|
146
|
+
comment: true
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
| Input | Default | Purpose |
|
|
150
|
+
| --- | --- | --- |
|
|
151
|
+
| `paths` | *(whole project)* | Files or directories to scan |
|
|
152
|
+
| `fail-on` | `critical` | Fail the job at or above this severity (`critical`/`high`/`medium`/`low`/`none`) |
|
|
153
|
+
| `comment` | `true` | Post/update a sticky summary comment on the PR |
|
|
154
|
+
| `sarif` | `false` | Upload results to GitHub code scanning |
|
|
155
|
+
| `offline` | `false` | Skip registry and OSV lookups |
|
|
156
|
+
| `working-directory` | `.` | Directory to scan from |
|
|
157
|
+
|
|
158
|
+
Outputs `verdict` (`clear`/`conditional`/`hold`), `critical`, `high` and `total`
|
|
159
|
+
for use in later steps. The comment is *sticky* — re-runs edit the same comment
|
|
160
|
+
instead of piling up. Until the npm package is published, the action builds
|
|
161
|
+
itself from its own checkout, so `uses: …@ref` works immediately.
|
|
162
|
+
|
|
163
|
+
### Plain CLI
|
|
164
|
+
|
|
165
|
+
```yaml
|
|
166
|
+
- run: npx cleartoship --fail-on=critical
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
To feed findings into GitHub's Security tab without the Action:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
- run: npx cleartoship --sarif -o results.sarif --fail-on=none
|
|
173
|
+
- uses: github/codeql-action/upload-sarif@v3
|
|
174
|
+
with: { sarif_file: results.sarif }
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## How it works## How it works
|
|
178
|
+
|
|
179
|
+
Four scanners, all static — nothing is uploaded and no database is contacted.
|
|
180
|
+
|
|
181
|
+
1. **Server Actions & Route Handlers** — parses TS/TSX with Babel, finds every exported
|
|
182
|
+
function reachable over HTTP (`'use server'` modules, inline directives, `app/**/route.ts`
|
|
183
|
+
method exports), then asks whether it authenticates, validates and scopes its writes.
|
|
184
|
+
Recognises `next-safe-action` / `zsa` style wrappers so wrapped actions are not
|
|
185
|
+
double-reported.
|
|
186
|
+
2. **Row Level Security** — replays your `.sql` migrations in filename order to build a model
|
|
187
|
+
of the resulting schema (tables, columns, RLS state, policies, grants, views, functions),
|
|
188
|
+
then judges the end state. This is the same class of check as Supabase's own `splinter`
|
|
189
|
+
linter, but static, so it runs on a pull request with no live database.
|
|
190
|
+
3. **Dependency hallucination** — resolves every declared dependency against npm and PyPI,
|
|
191
|
+
flagging names that do not exist at all, names registered days ago with no users, and names
|
|
192
|
+
one edit away from a popular package. Results are cached for 24h under
|
|
193
|
+
`~/.cache/cleartoship`.
|
|
194
|
+
4. **Secrets & client boundary** — pattern plus verification: candidate JWTs are decoded and
|
|
195
|
+
only reported when the payload actually says `role: service_role`. 15 hand-tuned patterns
|
|
196
|
+
cover the providers that matter most; 219 more come from the vendored gitleaks ruleset,
|
|
197
|
+
each gated on a keyword prefilter and a Shannon entropy threshold so that
|
|
198
|
+
`your_api_key_here` never reads as a breach. Values in test fixtures, docs and
|
|
199
|
+
commented-out counter-examples are downgraded rather than reported.
|
|
200
|
+
5. **Known vulnerabilities** — dependency versions are resolved from
|
|
201
|
+
`package-lock.json` / `pnpm-lock.yaml` / `yarn.lock` (falling back to the range floor)
|
|
202
|
+
and queried against OSV.dev, the database behind Google's `osv-scanner`. Live data
|
|
203
|
+
beats hand-written version regexes, which go stale the week they are written — so when
|
|
204
|
+
OSV answers, the vendored CVE rules stand down. `--offline` reverses that.
|
|
205
|
+
|
|
206
|
+
### Design notes
|
|
207
|
+
|
|
208
|
+
- **Fail open on uncertainty.** If a registry lookup fails, the package is treated as valid —
|
|
209
|
+
a network blip must never be reported as a hallucinated dependency.
|
|
210
|
+
- **Test fixtures are not breaches.** Credentials under `tests/`, `fixtures/`, `docs/` or in a
|
|
211
|
+
commented-out line are reported at `low`, never as blocking criticals.
|
|
212
|
+
- **Precision over recall on the noisy rules.** Typosquat matching skips exact matches and
|
|
213
|
+
names shorter than five characters, where one-edit neighbours are meaningless.
|
|
214
|
+
|
|
215
|
+
## Programmatic use
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { scan, renderJson } from 'cleartoship';
|
|
219
|
+
|
|
220
|
+
const result = await scan({ root: process.cwd(), offline: true });
|
|
221
|
+
console.log(result.counts); // { critical: 0, high: 2, medium: 1, low: 0, info: 0 }
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Exit codes
|
|
225
|
+
|
|
226
|
+
| Code | Meaning |
|
|
227
|
+
| --- | --- |
|
|
228
|
+
| `0` | Clear to ship at the configured `--fail-on` threshold |
|
|
229
|
+
| `1` | Findings at or above the threshold |
|
|
230
|
+
| `2` | The scanner itself errored |
|
|
231
|
+
|
|
232
|
+
## Prior art, and what was taken from where
|
|
233
|
+
|
|
234
|
+
ClearToShip occupies the gap between general-purpose scanners and the modern AI-assisted
|
|
235
|
+
stack. It is not a replacement for [Semgrep](https://github.com/semgrep/semgrep),
|
|
236
|
+
[Trivy](https://github.com/aquasecurity/trivy), [OSV-Scanner](https://github.com/google/osv-scanner)
|
|
237
|
+
or [TruffleHog](https://github.com/trufflesecurity/trufflehog) — run those too. It answers a
|
|
238
|
+
narrower question they don't: *given that an LLM wrote this, what did it forget?*
|
|
239
|
+
|
|
240
|
+
The rule set was designed after surveying the field. What each source contributed:
|
|
241
|
+
|
|
242
|
+
| Source | License | What was taken |
|
|
243
|
+
| --- | --- | --- |
|
|
244
|
+
| [supabase/splinter](https://github.com/supabase/splinter) | none stated | The vulnerability classes behind CTS010–019 and CTS050–052. Splinter runs SQL against a live database; these are static reimplementations against migration files. Verified by pointing ClearToShip at splinter's own `test/sql/` fixtures. |
|
|
245
|
+
| [slopcheck](https://github.com/mattschaller/slopcheck) | MIT | Three distinctions worth making: HTTP 451 (pulled for malware) ≠ 404 (never existed) ≠ unpublished-with-installs (open to takeover) — CTS026/CTS027. And the idea of reading install commands out of prose and agent instruction files. |
|
|
246
|
+
| [guardvibe](https://github.com/goklab/guardvibe) | Apache-2.0 | Coverage gaps: webhook signature verification, cron secrets, mass assignment, `dangerouslyAllowBrowser`, schema escape hatches, `getSession` vs `getUser` — CTS040–046. |
|
|
247
|
+
|
|
248
|
+
**No rule content was copied.** Every check here is an independent implementation, and the
|
|
249
|
+
detection engine is different in kind: ClearToShip parses TypeScript to an AST and replays SQL
|
|
250
|
+
migrations into a schema model, where the regex-and-window approach used by several of these
|
|
251
|
+
tools cannot express "this function has no auth check anywhere in its body".
|
|
252
|
+
|
|
253
|
+
That distinction is not just technical pride — it is a licensing constraint. Two of the most
|
|
254
|
+
tempting corpora, **TruffleHog** (800+ verified credential detectors) and
|
|
255
|
+
**RouteWarden**, are **AGPL-3.0**; lifting their detectors into a commercial product carries
|
|
256
|
+
the AGPL's network-use obligations. **semgrep-rules** ships under the bespoke *Semgrep Rules
|
|
257
|
+
License v1.0*, which needs reading before any rule is reused. **splinter** and
|
|
258
|
+
**supabase-exposure-check** publish no license at all, which means default copyright — ideas
|
|
259
|
+
are free, expression is not. Vulnerability classes are facts and cannot be owned; regexes,
|
|
260
|
+
queries and rule text can be.
|
|
261
|
+
|
|
262
|
+
## License
|
|
263
|
+
|
|
264
|
+
MIT
|
package/action.yml
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
name: ClearToShip
|
|
2
|
+
description: Pre-flight security check for AI-built & vibe-coded apps. Blocks PRs on missing Server Action auth, Supabase RLS holes, hallucinated packages and leaked keys.
|
|
3
|
+
author: ClearToShip
|
|
4
|
+
branding:
|
|
5
|
+
icon: shield
|
|
6
|
+
color: green
|
|
7
|
+
|
|
8
|
+
inputs:
|
|
9
|
+
paths:
|
|
10
|
+
description: Files or directories to scan, space-separated. Defaults to the whole project.
|
|
11
|
+
required: false
|
|
12
|
+
default: ''
|
|
13
|
+
fail-on:
|
|
14
|
+
description: Fail the job at or above this severity (critical, high, medium, low, none).
|
|
15
|
+
required: false
|
|
16
|
+
default: critical
|
|
17
|
+
comment:
|
|
18
|
+
description: Post (and update) a summary comment on the pull request. Requires pull-requests write permission.
|
|
19
|
+
required: false
|
|
20
|
+
default: 'true'
|
|
21
|
+
sarif:
|
|
22
|
+
description: Write results.sarif for upload to GitHub code scanning.
|
|
23
|
+
required: false
|
|
24
|
+
default: 'false'
|
|
25
|
+
offline:
|
|
26
|
+
description: Skip registry and OSV lookups.
|
|
27
|
+
required: false
|
|
28
|
+
default: 'false'
|
|
29
|
+
version:
|
|
30
|
+
description: Version of the cleartoship npm package to run.
|
|
31
|
+
required: false
|
|
32
|
+
default: latest
|
|
33
|
+
working-directory:
|
|
34
|
+
description: Directory to run the scan from.
|
|
35
|
+
required: false
|
|
36
|
+
default: '.'
|
|
37
|
+
github-token:
|
|
38
|
+
description: Token used to post the PR comment.
|
|
39
|
+
required: false
|
|
40
|
+
default: ${{ github.token }}
|
|
41
|
+
|
|
42
|
+
outputs:
|
|
43
|
+
verdict:
|
|
44
|
+
description: One of clear, conditional, or hold.
|
|
45
|
+
value: ${{ steps.scan.outputs.verdict }}
|
|
46
|
+
critical:
|
|
47
|
+
description: Number of critical findings.
|
|
48
|
+
value: ${{ steps.scan.outputs.critical }}
|
|
49
|
+
high:
|
|
50
|
+
description: Number of high findings.
|
|
51
|
+
value: ${{ steps.scan.outputs.high }}
|
|
52
|
+
total:
|
|
53
|
+
description: Total number of findings.
|
|
54
|
+
value: ${{ steps.scan.outputs.total }}
|
|
55
|
+
|
|
56
|
+
runs:
|
|
57
|
+
using: composite
|
|
58
|
+
steps:
|
|
59
|
+
- name: Resolve ClearToShip
|
|
60
|
+
id: resolve
|
|
61
|
+
shell: bash
|
|
62
|
+
run: |
|
|
63
|
+
# Prefer the published npm package. If it cannot be resolved — which is
|
|
64
|
+
# the case before the first publish, or when a pinned version is not on
|
|
65
|
+
# the registry — fall back to building from this action's own checkout,
|
|
66
|
+
# so `uses: <owner>/cleartoship@ref` works with no npm dependency.
|
|
67
|
+
ver="${{ inputs.version }}"
|
|
68
|
+
if [ "$ver" != "local" ] && npm view "cleartoship@${ver}" version >/dev/null 2>&1; then
|
|
69
|
+
echo "cmd=npx --yes cleartoship@${ver}" >> "$GITHUB_OUTPUT"
|
|
70
|
+
echo "Using published cleartoship@${ver}"
|
|
71
|
+
else
|
|
72
|
+
echo "Published package not found; building from the action checkout."
|
|
73
|
+
( cd "$GITHUB_ACTION_PATH" && npm ci --silent && npm run build --silent )
|
|
74
|
+
echo "cmd=node ${GITHUB_ACTION_PATH}/dist/cli.js" >> "$GITHUB_OUTPUT"
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
- name: Run ClearToShip
|
|
78
|
+
id: scan
|
|
79
|
+
shell: bash
|
|
80
|
+
working-directory: ${{ inputs.working-directory }}
|
|
81
|
+
run: |
|
|
82
|
+
set -o pipefail
|
|
83
|
+
CTS="${{ steps.resolve.outputs.cmd }}"
|
|
84
|
+
args=()
|
|
85
|
+
[ -n "${{ inputs.paths }}" ] && args+=(${{ inputs.paths }})
|
|
86
|
+
[ "${{ inputs.offline }}" = "true" ] && args+=(--offline)
|
|
87
|
+
|
|
88
|
+
# The gate is applied at the end so the report and comment are produced
|
|
89
|
+
# regardless of pass/fail; --fail-on=none keeps this step from aborting early.
|
|
90
|
+
$CTS "${args[@]}" --json --fail-on=none > cleartoship.json
|
|
91
|
+
$CTS "${args[@]}" --markdown --fail-on=none > cleartoship.md || true
|
|
92
|
+
if [ "${{ inputs.sarif }}" = "true" ]; then
|
|
93
|
+
$CTS "${args[@]}" --sarif --fail-on=none > results.sarif || true
|
|
94
|
+
fi
|
|
95
|
+
|
|
96
|
+
node -e '
|
|
97
|
+
const r = require("./cleartoship.json");
|
|
98
|
+
const out = process.env.GITHUB_OUTPUT;
|
|
99
|
+
const fs = require("fs");
|
|
100
|
+
fs.appendFileSync(out, `verdict=${r.verdict}\n`);
|
|
101
|
+
fs.appendFileSync(out, `critical=${r.counts.critical}\n`);
|
|
102
|
+
fs.appendFileSync(out, `high=${r.counts.high}\n`);
|
|
103
|
+
fs.appendFileSync(out, `total=${r.findings.length}\n`);
|
|
104
|
+
'
|
|
105
|
+
|
|
106
|
+
# Job summary — always shown on the run.
|
|
107
|
+
cat cleartoship.md >> "$GITHUB_STEP_SUMMARY"
|
|
108
|
+
|
|
109
|
+
- name: Comment on pull request
|
|
110
|
+
if: ${{ inputs.comment == 'true' && github.event_name == 'pull_request' }}
|
|
111
|
+
shell: bash
|
|
112
|
+
working-directory: ${{ inputs.working-directory }}
|
|
113
|
+
env:
|
|
114
|
+
GH_TOKEN: ${{ inputs.github-token }}
|
|
115
|
+
PR: ${{ github.event.pull_request.number }}
|
|
116
|
+
run: |
|
|
117
|
+
# A sticky comment: find a previous ClearToShip comment and edit it, so
|
|
118
|
+
# re-runs update in place instead of piling up.
|
|
119
|
+
marker="<!-- cleartoship -->"
|
|
120
|
+
printf '%s\n\n' "$marker" > body.md
|
|
121
|
+
cat cleartoship.md >> body.md
|
|
122
|
+
|
|
123
|
+
id=$(gh api "repos/${{ github.repository }}/issues/${PR}/comments" \
|
|
124
|
+
--jq "map(select(.body | contains(\"$marker\"))) | .[0].id // empty" 2>/dev/null || true)
|
|
125
|
+
if [ -n "$id" ]; then
|
|
126
|
+
gh api -X PATCH "repos/${{ github.repository }}/issues/comments/${id}" -F body=@body.md >/dev/null
|
|
127
|
+
else
|
|
128
|
+
gh api -X POST "repos/${{ github.repository }}/issues/${PR}/comments" -F body=@body.md >/dev/null
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
- name: Upload SARIF
|
|
132
|
+
if: ${{ inputs.sarif == 'true' && always() }}
|
|
133
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
134
|
+
with:
|
|
135
|
+
sarif_file: ${{ inputs.working-directory }}/results.sarif
|
|
136
|
+
|
|
137
|
+
- name: Apply severity gate
|
|
138
|
+
shell: bash
|
|
139
|
+
run: |
|
|
140
|
+
gate="${{ inputs.fail-on }}"
|
|
141
|
+
[ "$gate" = "none" ] && exit 0
|
|
142
|
+
declare -A rank=( [critical]=4 [high]=3 [medium]=2 [low]=1 )
|
|
143
|
+
floor=${rank[$gate]:-4}
|
|
144
|
+
crit=${{ steps.scan.outputs.critical }}
|
|
145
|
+
high=${{ steps.scan.outputs.high }}
|
|
146
|
+
# Only critical and high are wired as outputs; medium/low never block by default.
|
|
147
|
+
blocking=0
|
|
148
|
+
[ "$floor" -le 4 ] && blocking=$((blocking + crit))
|
|
149
|
+
[ "$floor" -le 3 ] && blocking=$((blocking + high))
|
|
150
|
+
if [ "$blocking" -gt 0 ]; then
|
|
151
|
+
echo "::error::ClearToShip found $blocking finding(s) at or above '$gate'."
|
|
152
|
+
exit 1
|
|
153
|
+
fi
|
|
154
|
+
echo "ClearToShip: clear to ship at '$gate'."
|
package/dist/banner.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function banner(version: string): string;
|
package/dist/banner.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import pc from 'picocolors';
|
|
2
|
+
const ART = String.raw `
|
|
3
|
+
___ _ _____ ___ _ _
|
|
4
|
+
/ __| |___ __ _ _ _|_ _|__ / __| |_ (_)_ __
|
|
5
|
+
| (__| / -_) _' | '_| | |/ _ \ \__ \ ' \| | '_ \
|
|
6
|
+
\___|_\___\__,_|_| |_|\___/ |___/_||_|_| .__/
|
|
7
|
+
|_|`;
|
|
8
|
+
export function banner(version) {
|
|
9
|
+
return (pc.cyan(ART) +
|
|
10
|
+
'\n ' +
|
|
11
|
+
pc.dim('Pre-flight security check for AI-built & vibe-coded apps') +
|
|
12
|
+
pc.dim(` • v${version}`) +
|
|
13
|
+
'\n');
|
|
14
|
+
}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command, Option } from 'commander';
|
|
3
|
+
import { writeFileSync, readFileSync } from 'node:fs';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
import { scan } from './scan.js';
|
|
8
|
+
import { banner } from './banner.js';
|
|
9
|
+
import { renderTerminal, renderJson, renderSarif, renderFixPrompt, renderBadge, renderMarkdown, } from './report.js';
|
|
10
|
+
import { SEVERITY_ORDER } from './types.js';
|
|
11
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
12
|
+
let version = '0.0.0';
|
|
13
|
+
try {
|
|
14
|
+
version = JSON.parse(readFileSync(join(here, '..', 'package.json'), 'utf8')).version;
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
/* running from an unusual layout; version is cosmetic */
|
|
18
|
+
}
|
|
19
|
+
const program = new Command();
|
|
20
|
+
program
|
|
21
|
+
.name('cleartoship')
|
|
22
|
+
.description('Pre-flight security check for AI-built & vibe-coded apps.\n' +
|
|
23
|
+
'Finds missing Server Action auth, Supabase RLS holes, hallucinated npm\n' +
|
|
24
|
+
'packages and leaked keys before you deploy.')
|
|
25
|
+
.version(version, '-v, --version')
|
|
26
|
+
.argument('[paths...]', 'files or directories to scan (default: the whole project)')
|
|
27
|
+
.option('-C, --cwd <dir>', 'project root', process.cwd())
|
|
28
|
+
.addOption(new Option('--fail-on <severity>', 'exit non-zero at or above this severity')
|
|
29
|
+
.choices(['critical', 'high', 'medium', 'low', 'none'])
|
|
30
|
+
.default('critical'))
|
|
31
|
+
.addOption(new Option('--min-severity <severity>', 'hide findings below this severity')
|
|
32
|
+
.choices(['critical', 'high', 'medium', 'low', 'info'])
|
|
33
|
+
.default('low'))
|
|
34
|
+
.option('--json', 'emit machine-readable JSON instead of the report')
|
|
35
|
+
.option('--sarif', 'emit SARIF 2.1.0 (upload to GitHub code scanning)')
|
|
36
|
+
.option('--fix-prompt', 'emit a ready-to-paste prompt for Cursor / Claude Code')
|
|
37
|
+
.option('--badge', 'print the markdown status badge for your README')
|
|
38
|
+
.option('--markdown', 'emit a markdown report (for PR comments / job summaries)')
|
|
39
|
+
.option('-o, --output <file>', 'write the chosen output to a file instead of stdout')
|
|
40
|
+
.option('--offline', 'skip registry lookups (no network)')
|
|
41
|
+
.option('--no-community', 'run only ClearToShip rules, skipping the vendored community ruleset')
|
|
42
|
+
.option('--ignore <ids>', 'comma-separated rule ids to skip, e.g. CTS004,CTS022')
|
|
43
|
+
.option('--only <ids>', 'comma-separated rule ids to report exclusively')
|
|
44
|
+
.option('--no-banner', 'suppress the ASCII header')
|
|
45
|
+
.option('--quiet', 'only print findings, no passed checks')
|
|
46
|
+
.option('--verbose', 'extra diagnostic output')
|
|
47
|
+
.action(async (paths, opts) => {
|
|
48
|
+
const machineReadable = Boolean(opts.json || opts.sarif || opts.fixPrompt || opts.badge || opts.markdown);
|
|
49
|
+
const interactive = !machineReadable && !opts.output;
|
|
50
|
+
if (interactive && opts.banner !== false) {
|
|
51
|
+
process.stderr.write(banner(version) + '\n');
|
|
52
|
+
}
|
|
53
|
+
const list = (value) => value ? value.split(',').map((s) => s.trim()).filter(Boolean) : undefined;
|
|
54
|
+
const result = await scan({
|
|
55
|
+
root: opts.cwd,
|
|
56
|
+
paths,
|
|
57
|
+
offline: opts.offline,
|
|
58
|
+
noCommunity: opts.community === false,
|
|
59
|
+
ignore: list(opts.ignore),
|
|
60
|
+
only: list(opts.only),
|
|
61
|
+
minSeverity: opts.minSeverity,
|
|
62
|
+
verbose: opts.verbose,
|
|
63
|
+
onProgress: interactive
|
|
64
|
+
? (step, total, name) => {
|
|
65
|
+
process.stderr.write(pc.dim(` [${step}/${total}] ${name}…\n`));
|
|
66
|
+
}
|
|
67
|
+
: undefined,
|
|
68
|
+
});
|
|
69
|
+
if (interactive) {
|
|
70
|
+
process.stderr.write(`\n ${pc.dim('root')} ${result.root}\n` +
|
|
71
|
+
` ${pc.dim('stack')} ${result.framework}\n`);
|
|
72
|
+
}
|
|
73
|
+
let output;
|
|
74
|
+
if (opts.json)
|
|
75
|
+
output = renderJson(result);
|
|
76
|
+
else if (opts.sarif)
|
|
77
|
+
output = renderSarif(result, version);
|
|
78
|
+
else if (opts.fixPrompt)
|
|
79
|
+
output = renderFixPrompt(result);
|
|
80
|
+
else if (opts.badge)
|
|
81
|
+
output = renderBadge(result);
|
|
82
|
+
else if (opts.markdown)
|
|
83
|
+
output = renderMarkdown(result);
|
|
84
|
+
else
|
|
85
|
+
output = renderTerminal(result, { showPassed: !opts.quiet });
|
|
86
|
+
if (opts.output) {
|
|
87
|
+
writeFileSync(opts.output, output.endsWith('\n') ? output : output + '\n');
|
|
88
|
+
process.stderr.write(pc.dim(`\n wrote ${opts.output}\n`));
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
process.stdout.write(output.endsWith('\n') ? output : output + '\n');
|
|
92
|
+
}
|
|
93
|
+
if (opts.failOn === 'none')
|
|
94
|
+
return;
|
|
95
|
+
const floor = SEVERITY_ORDER[opts.failOn];
|
|
96
|
+
const blocking = result.findings.filter((f) => SEVERITY_ORDER[f.severity] >= floor).length;
|
|
97
|
+
if (blocking > 0)
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
});
|
|
100
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
101
|
+
process.stderr.write(pc.red(`cleartoship: ${err instanceof Error ? err.message : String(err)}\n`));
|
|
102
|
+
process.exitCode = 2;
|
|
103
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frequently-installed packages, used as typosquat bait references. A candidate
|
|
3
|
+
* that sits one edit away from one of these — and has almost no downloads of its
|
|
4
|
+
* own — is very likely a squat or an LLM misremembering the real name.
|
|
5
|
+
*/
|
|
6
|
+
export declare const POPULAR_NPM: string[];
|
|
7
|
+
export declare const POPULAR_PYPI: string[];
|
|
8
|
+
/** Damerau-Levenshtein distance, capped: returns `max + 1` once it exceeds `max`. */
|
|
9
|
+
export declare function editDistance(a: string, b: string, max?: number): number;
|
|
10
|
+
/**
|
|
11
|
+
* Closest popular package within `max` edits, or null.
|
|
12
|
+
*
|
|
13
|
+
* A name that *is* one of the popular packages is never its own lookalike, and
|
|
14
|
+
* names shorter than five characters are skipped: at that length almost every
|
|
15
|
+
* real package has a one-edit neighbour, so the signal is noise.
|
|
16
|
+
*/
|
|
17
|
+
export declare function nearestPopular(name: string, list: string[], max?: number): string | null;
|