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.
Files changed (105) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +264 -0
  3. package/action.yml +154 -0
  4. package/dist/banner.d.ts +1 -0
  5. package/dist/banner.js +14 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +103 -0
  8. package/dist/data/popular.d.ts +17 -0
  9. package/dist/data/popular.js +103 -0
  10. package/dist/index.d.ts +5 -0
  11. package/dist/index.js +4 -0
  12. package/dist/report.d.ts +15 -0
  13. package/dist/report.js +317 -0
  14. package/dist/scan.d.ts +24 -0
  15. package/dist/scan.js +81 -0
  16. package/dist/scanners/community.d.ts +2 -0
  17. package/dist/scanners/community.js +155 -0
  18. package/dist/scanners/dependencies.d.ts +21 -0
  19. package/dist/scanners/dependencies.js +549 -0
  20. package/dist/scanners/index.d.ts +8 -0
  21. package/dist/scanners/index.js +13 -0
  22. package/dist/scanners/rls.d.ts +7 -0
  23. package/dist/scanners/rls.js +531 -0
  24. package/dist/scanners/secrets.d.ts +2 -0
  25. package/dist/scanners/secrets.js +439 -0
  26. package/dist/scanners/server-actions.d.ts +2 -0
  27. package/dist/scanners/server-actions.js +466 -0
  28. package/dist/types.d.ts +65 -0
  29. package/dist/types.js +10 -0
  30. package/dist/utils/ast.d.ts +13 -0
  31. package/dist/utils/ast.js +107 -0
  32. package/dist/utils/detect.d.ts +2 -0
  33. package/dist/utils/detect.js +77 -0
  34. package/dist/utils/entropy.d.ts +7 -0
  35. package/dist/utils/entropy.js +19 -0
  36. package/dist/utils/files.d.ts +13 -0
  37. package/dist/utils/files.js +154 -0
  38. package/dist/utils/osv.d.ts +26 -0
  39. package/dist/utils/osv.js +123 -0
  40. package/dist/utils/registry.d.ts +44 -0
  41. package/dist/utils/registry.js +195 -0
  42. package/dist/utils/sql.d.ts +20 -0
  43. package/dist/utils/sql.js +157 -0
  44. package/dist/utils/suppress.d.ts +19 -0
  45. package/dist/utils/suppress.js +56 -0
  46. package/dist/utils/traverse.d.ts +2 -0
  47. package/dist/utils/traverse.js +14 -0
  48. package/dist/vendor/gitleaks/rules.d.ts +13 -0
  49. package/dist/vendor/gitleaks/rules.js +1571 -0
  50. package/dist/vendor/guardvibe/index.d.ts +11 -0
  51. package/dist/vendor/guardvibe/index.js +60 -0
  52. package/dist/vendor/guardvibe/rules/advanced-security.d.ts +2 -0
  53. package/dist/vendor/guardvibe/rules/advanced-security.js +418 -0
  54. package/dist/vendor/guardvibe/rules/ai-host-security.d.ts +2 -0
  55. package/dist/vendor/guardvibe/rules/ai-host-security.js +190 -0
  56. package/dist/vendor/guardvibe/rules/ai-security.d.ts +2 -0
  57. package/dist/vendor/guardvibe/rules/ai-security.js +413 -0
  58. package/dist/vendor/guardvibe/rules/ai-tool-runtime.d.ts +2 -0
  59. package/dist/vendor/guardvibe/rules/ai-tool-runtime.js +194 -0
  60. package/dist/vendor/guardvibe/rules/api-security.d.ts +2 -0
  61. package/dist/vendor/guardvibe/rules/api-security.js +154 -0
  62. package/dist/vendor/guardvibe/rules/auth.d.ts +2 -0
  63. package/dist/vendor/guardvibe/rules/auth.js +222 -0
  64. package/dist/vendor/guardvibe/rules/cicd.d.ts +2 -0
  65. package/dist/vendor/guardvibe/rules/cicd.js +108 -0
  66. package/dist/vendor/guardvibe/rules/core.d.ts +2 -0
  67. package/dist/vendor/guardvibe/rules/core.js +484 -0
  68. package/dist/vendor/guardvibe/rules/cve-versions.d.ts +2 -0
  69. package/dist/vendor/guardvibe/rules/cve-versions.js +1130 -0
  70. package/dist/vendor/guardvibe/rules/database.d.ts +2 -0
  71. package/dist/vendor/guardvibe/rules/database.js +173 -0
  72. package/dist/vendor/guardvibe/rules/deployment.d.ts +2 -0
  73. package/dist/vendor/guardvibe/rules/deployment.js +269 -0
  74. package/dist/vendor/guardvibe/rules/dockerfile.d.ts +2 -0
  75. package/dist/vendor/guardvibe/rules/dockerfile.js +99 -0
  76. package/dist/vendor/guardvibe/rules/firebase.d.ts +2 -0
  77. package/dist/vendor/guardvibe/rules/firebase.js +96 -0
  78. package/dist/vendor/guardvibe/rules/go.d.ts +2 -0
  79. package/dist/vendor/guardvibe/rules/go.js +85 -0
  80. package/dist/vendor/guardvibe/rules/modern-stack.d.ts +2 -0
  81. package/dist/vendor/guardvibe/rules/modern-stack.js +631 -0
  82. package/dist/vendor/guardvibe/rules/nextjs.d.ts +2 -0
  83. package/dist/vendor/guardvibe/rules/nextjs.js +233 -0
  84. package/dist/vendor/guardvibe/rules/other-services.d.ts +2 -0
  85. package/dist/vendor/guardvibe/rules/other-services.js +72 -0
  86. package/dist/vendor/guardvibe/rules/payments.d.ts +2 -0
  87. package/dist/vendor/guardvibe/rules/payments.js +123 -0
  88. package/dist/vendor/guardvibe/rules/react-native.d.ts +2 -0
  89. package/dist/vendor/guardvibe/rules/react-native.js +132 -0
  90. package/dist/vendor/guardvibe/rules/services.d.ts +2 -0
  91. package/dist/vendor/guardvibe/rules/services.js +149 -0
  92. package/dist/vendor/guardvibe/rules/shell.d.ts +2 -0
  93. package/dist/vendor/guardvibe/rules/shell.js +72 -0
  94. package/dist/vendor/guardvibe/rules/sql.d.ts +2 -0
  95. package/dist/vendor/guardvibe/rules/sql.js +60 -0
  96. package/dist/vendor/guardvibe/rules/supply-chain.d.ts +2 -0
  97. package/dist/vendor/guardvibe/rules/supply-chain.js +243 -0
  98. package/dist/vendor/guardvibe/rules/terraform.d.ts +2 -0
  99. package/dist/vendor/guardvibe/rules/terraform.js +84 -0
  100. package/dist/vendor/guardvibe/rules/types.d.ts +14 -0
  101. package/dist/vendor/guardvibe/rules/types.js +11 -0
  102. package/dist/vendor/guardvibe/rules/web-security.d.ts +2 -0
  103. package/dist/vendor/guardvibe/rules/web-security.js +258 -0
  104. package/examples/security.yml +23 -0
  105. 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'."
@@ -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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
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;