cleartoship 0.12.0 → 0.12.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.md +20 -36
- package/action.yml +2 -2
- package/dist/scanners/community.js +33 -0
- package/dist/scanners/logic.js +4 -3
- package/dist/types.d.ts +1 -0
- package/dist/utils/detect.js +6 -0
- package/dist/utils/owasp.d.ts +21 -11
- package/dist/utils/owasp.js +33 -17
- package/examples/security.yml +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -84,8 +84,8 @@ reaches a manifest.
|
|
|
84
84
|
| Rule | Severity | What it catches |
|
|
85
85
|
| --- | --- | --- |
|
|
86
86
|
| **CTS080** | high | Caller-supplied text interpolated into the instruction text itself — prompt injection by construction, not by filter (LLM01) |
|
|
87
|
-
| **CTS081** | medium | A request-reachable model call with no `max_tokens` ceiling: the answer's length, and its bill, chosen by whoever wrote the input (
|
|
88
|
-
| **CTS082** | medium | A system prompt in a `'use client'` module — compiled into the bundle, readable in devtools (
|
|
87
|
+
| **CTS081** | medium | A request-reachable model call with no `max_tokens` ceiling: the answer's length, and its bill, chosen by whoever wrote the input (LLM06) |
|
|
88
|
+
| **CTS082** | medium | A system prompt in a `'use client'` module — compiled into the bundle, readable in devtools (LLM08) |
|
|
89
89
|
|
|
90
90
|
**Logging, error-handling & deserialization** — the detectable slices of A08/A09/A10
|
|
91
91
|
|
|
@@ -147,54 +147,38 @@ A05 112, A04 78, A02 59, A03 32, A07 17, A06 13, A08 11, A09 2. ClearToShip's
|
|
|
147
147
|
own 45 rules add A01 19, A03 9, A04 7, A08 3, A05 2, and one each for A07, A09
|
|
148
148
|
and A10 — which is the category no vendored rule reaches.
|
|
149
149
|
|
|
150
|
-
## OWASP Top 10 for LLM Applications — coverage
|
|
150
|
+
## OWASP Top 10 for LLM Applications (2026) — coverage
|
|
151
151
|
|
|
152
152
|
Worth stating separately, because "we cover the OWASP Top 10" and "we cover the
|
|
153
|
-
LLM Top 10" are different claims and only one of them is usually meant.
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
153
|
+
LLM Top 10" are different claims and only one of them is usually meant. Mapped
|
|
154
|
+
against the **2026 edition**, published 4 August 2026 — which renumbered eight
|
|
155
|
+
of the ten: Excessive Agency moved 06 → 03, Unbounded Consumption 10 → 06,
|
|
156
|
+
Improper Output Handling 05 → 10, and System Prompt Leakage was renamed and
|
|
157
|
+
broadened into **Hidden Context Exposure**.
|
|
157
158
|
|
|
158
159
|
| Category | Rules | What we detect |
|
|
159
160
|
| --- | --- | --- |
|
|
160
161
|
| **LLM01** Prompt Injection | 12 + **CTS080** | Caller text interpolated into the instruction text itself; fetched pages and query results reaching a prompt unbounded; instructions hidden in a tool description |
|
|
161
162
|
| **LLM02** Sensitive Information Disclosure | 11 (+ CTS030, CTS045) | Provider keys in client code or a `NEXT_PUBLIC_` variable, `dangerouslyAllowBrowser`, a base URL pointed at somebody else's endpoint |
|
|
162
|
-
| **
|
|
163
|
-
| **
|
|
164
|
-
| **
|
|
165
|
-
| **
|
|
166
|
-
| **
|
|
167
|
-
| **LLM10**
|
|
168
|
-
| **
|
|
169
|
-
| **
|
|
163
|
+
| **LLM03** Excessive Agency | 9 | MCP servers with permissive tool access, `allowedTools` wildcards, auto-approve bypassing the permission prompt, settings hooks that fetch or pipe |
|
|
164
|
+
| **LLM04** Supply Chain | 1 | MCP server pinned to `@latest` |
|
|
165
|
+
| **LLM06** Unbounded Consumption | **CTS081** | A request-reachable model call with no `max_tokens` ceiling — the answer's length, and its cost, decided by whoever wrote the input |
|
|
166
|
+
| **LLM08** Hidden Context Exposure | 1 + **CTS082** | A system prompt held in a `'use client'` module, so it ships in the bundle; a prompt returned in an error response |
|
|
167
|
+
| **LLM09** Vector & Embedding Weaknesses | 3 | Retrieval results interpolated into a prompt, unauthenticated vector upserts |
|
|
168
|
+
| **LLM10** Improper Output Handling | 4 | Model output rendered as raw HTML or markdown images, or used in a dangerous sink |
|
|
169
|
+
| **LLM05** Data & Model Poisoning | — | Needs training-pipeline and dataset provenance. Nothing in a web app's source tree answers it, and a rule that pretended otherwise would be box-checking |
|
|
170
|
+
| **LLM07** Misinformation | — | A property of what the model says, not of the code that calls it. The adjacent detectable case — model output driving a security decision — would be a real rule, and is not written yet |
|
|
170
171
|
|
|
171
172
|
The mapping is derived from each rule's own text rather than a hand-kept list of
|
|
172
173
|
ids, so re-vendoring upstream cannot silently drop it, and it is deliberately
|
|
173
174
|
conservative: a rule that does not clearly belong to a category gets none.
|
|
174
175
|
|
|
175
176
|
**Eight of ten have first-party or vendored detection. Two do not, and will not
|
|
176
|
-
get a rule for the sake of the table** —
|
|
177
|
-
the repository, and
|
|
177
|
+
get a rule for the sake of the table** — LLM05 needs artefacts that are not in
|
|
178
|
+
the repository, and LLM07 is about the truthfulness of an answer. A tool that
|
|
178
179
|
claimed those would be lying about what it checked, which is the failure mode
|
|
179
180
|
this project exists to avoid.
|
|
180
181
|
|
|
181
|
-
Two honest points a reviewer would raise, answered up front:
|
|
182
|
-
|
|
183
|
-
- **A06 and A09 are hard for _any_ static tool.** A06 (Insecure Design) is about
|
|
184
|
-
missing threat modeling — you cannot grep for "the developer didn't consider an
|
|
185
|
-
abuse case." A09 (Logging failures) is largely a runtime/ops concern. Semgrep,
|
|
186
|
-
Snyk and CodeQL have the same limits. ClearToShip covers the *detectable slices*
|
|
187
|
-
(secrets in logs, fail-open error handling) and is honest that the rest needs a
|
|
188
|
-
human threat model and runtime observability, not a scanner.
|
|
189
|
-
- **Coverage is uneven on purpose.** The thesis is "the gaps LLM-generated code
|
|
190
|
-
leaves," which cluster in A01/A03/A07/A04 — so that is where the rules cluster.
|
|
191
|
-
|
|
192
|
-
Running with `--no-community` (first-party rules only) covers **8 of 10**
|
|
193
|
-
categories directly (A01, A03, A04, A05, A07, A08, A09, A10); the community pack
|
|
194
|
-
adds A02 and broadens A05/A07.
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
182
|
## Usage
|
|
199
183
|
|
|
200
184
|
```bash
|
|
@@ -267,7 +251,7 @@ jobs:
|
|
|
267
251
|
runs-on: ubuntu-latest
|
|
268
252
|
steps:
|
|
269
253
|
- uses: actions/checkout@v7
|
|
270
|
-
- uses: murtazaozdemir/cleartoship@v0.12.
|
|
254
|
+
- uses: murtazaozdemir/cleartoship@v0.12.2
|
|
271
255
|
with:
|
|
272
256
|
fail-on: critical
|
|
273
257
|
comment: true
|
|
@@ -289,7 +273,7 @@ above `fail-on`) for use in later steps. The comment is *sticky* — re-runs edi
|
|
|
289
273
|
the same comment instead of piling up.
|
|
290
274
|
|
|
291
275
|
By default the action runs the scanner version its own ref declares, so
|
|
292
|
-
`@v0.12.
|
|
276
|
+
`@v0.12.2` runs `cleartoship@0.12.2` and pinning the ref pins the behaviour. If
|
|
293
277
|
that version is not on the registry, it builds from its own checkout instead, so
|
|
294
278
|
`uses: …@ref` works against an unpublished commit.
|
|
295
279
|
|
package/action.yml
CHANGED
|
@@ -29,7 +29,7 @@ inputs:
|
|
|
29
29
|
version:
|
|
30
30
|
description: >-
|
|
31
31
|
Version of the cleartoship npm package to run. Defaults to the version this
|
|
32
|
-
action's own ref declares, so `uses: …@v0.12.
|
|
32
|
+
action's own ref declares, so `uses: …@v0.12.2` runs cleartoship@0.12.2. Set
|
|
33
33
|
`latest` to always track the newest release, or `local` to build from the checkout.
|
|
34
34
|
required: false
|
|
35
35
|
default: ''
|
|
@@ -76,7 +76,7 @@ runs:
|
|
|
76
76
|
run: |
|
|
77
77
|
# With no version pinned, run the exact version this action's checkout
|
|
78
78
|
# declares. That keeps the action ref and the scanner in lockstep:
|
|
79
|
-
# `uses: <owner>/cleartoship@v0.12.
|
|
79
|
+
# `uses: <owner>/cleartoship@v0.12.2` runs cleartoship@0.12.2 instead of
|
|
80
80
|
# whatever npm happens to tag `latest` at the time.
|
|
81
81
|
ver="$INPUT_VERSION"
|
|
82
82
|
if [ -z "$ver" ]; then
|
|
@@ -254,6 +254,20 @@ const MATCH_GUARDS = {
|
|
|
254
254
|
// cannot tell whether the auth check is already there, and CTS001 can:
|
|
255
255
|
// matching on the module's own vocabulary keeps this from contradicting it.
|
|
256
256
|
VG1025: (_match, source) => !/\b(auth\.getUser|getServerSession|getServerAuthSession|requireUser|requireAuth|requireUserId|currentUser|handleSessionToken|verifyRequest|getSession)\s*\(/.test(source),
|
|
257
|
+
// `jwt.sign(payload, getJwtSecret(), { expiresIn: TOKEN_EXPIRY })` was
|
|
258
|
+
// reported as a token without expiry: the pattern's `[^)]` scan for
|
|
259
|
+
// `expiresIn` stops at the first `)`, which here belongs to `getJwtSecret()`.
|
|
260
|
+
// Read the whole call before saying the option is absent.
|
|
261
|
+
VG061: (_match, source, index) => !/\b(expiresIn|exp\s*:|setExpirationTime)\b/.test(source.slice(index, index + 300)),
|
|
262
|
+
// The "sink" half of the upload rule matched the word *upload* inside a
|
|
263
|
+
// sentence — "Please upload a CSV file exported from Shopify". A filename
|
|
264
|
+
// reaching a string is not a filename reaching a filesystem.
|
|
265
|
+
VG993: (match, source, index, spans) => !isInside(spans, index + match.length - 1, 'string'),
|
|
266
|
+
// A login endpoint is server code. This fired on `<Link href="/login">Back to
|
|
267
|
+
// sign in</Link>` in a client page, where "verify" and "login" are words in
|
|
268
|
+
// markup rather than a password comparison behind an HTTP handler.
|
|
269
|
+
VG148: (match, source) => !/^\s*(['"])use client\1/m.test(source.slice(0, 400)) &&
|
|
270
|
+
/(bcrypt\.compare|argon2\.verify|\b(compare|verify|verifyPassword)\s*\()/.test(match),
|
|
257
271
|
// SSRF is a *server* being made to fetch a URL it should not. A module marked
|
|
258
272
|
// `'use client'` runs in the browser, where the request leaves the user's own
|
|
259
273
|
// machine and crosses no trust boundary of yours.
|
|
@@ -298,6 +312,25 @@ function inapplicable(ctx) {
|
|
|
298
312
|
ids.add('VG132');
|
|
299
313
|
why.push('VG132 body-size limit (Next.js sets one by default)');
|
|
300
314
|
}
|
|
315
|
+
// A rule that names a platform cannot apply to a project that does not use
|
|
316
|
+
// it. "Supabase Auth Missing Middleware" was reported against an app with no
|
|
317
|
+
// Supabase dependency at all — advice about a library it does not import.
|
|
318
|
+
for (const [name, present, keyword] of [
|
|
319
|
+
['Supabase', ctx.framework.supabase, /supabase/i],
|
|
320
|
+
['Firebase', ctx.framework.firebase, /firebase|firestore/i],
|
|
321
|
+
]) {
|
|
322
|
+
if (present)
|
|
323
|
+
continue;
|
|
324
|
+
let n = 0;
|
|
325
|
+
for (const rule of GUARDVIBE_RULES) {
|
|
326
|
+
if (keyword.test(rule.name) || keyword.test(rule.description)) {
|
|
327
|
+
ids.add(rule.id);
|
|
328
|
+
n++;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (n > 0)
|
|
332
|
+
why.push(`${n} ${name} rules (no ${name} dependency)`);
|
|
333
|
+
}
|
|
301
334
|
return { ids, why };
|
|
302
335
|
}
|
|
303
336
|
/**
|
package/dist/scanners/logic.js
CHANGED
|
@@ -4,6 +4,7 @@ import { parseSource, calleeName, calleeTail } from '../utils/ast.js';
|
|
|
4
4
|
import { traverse } from '../utils/traverse.js';
|
|
5
5
|
import { Suppressions } from '../utils/suppress.js';
|
|
6
6
|
import { emptyResult } from '../types.js';
|
|
7
|
+
import { OWASP_LLM } from '../utils/owasp.js';
|
|
7
8
|
/**
|
|
8
9
|
* Cross-cutting logic checks for OWASP categories a pattern scanner can only
|
|
9
10
|
* touch in specific, high-signal slices:
|
|
@@ -181,7 +182,7 @@ export const logicScanner = {
|
|
|
181
182
|
line: path.node.loc?.start.line ?? 0,
|
|
182
183
|
cwe: 'CWE-200: Exposure of Sensitive Information to an Unauthorized Actor',
|
|
183
184
|
owasp: 'A04:2025 - Cryptographic Failures',
|
|
184
|
-
meta: { llm:
|
|
185
|
+
meta: { llm: OWASP_LLM.LLM08, binding: name },
|
|
185
186
|
});
|
|
186
187
|
},
|
|
187
188
|
CallExpression(path) {
|
|
@@ -209,7 +210,7 @@ export const logicScanner = {
|
|
|
209
210
|
line: node.loc?.start.line ?? 0,
|
|
210
211
|
cwe: 'CWE-770: Allocation of Resources Without Limits or Throttling',
|
|
211
212
|
owasp: 'A06:2025 - Insecure Design',
|
|
212
|
-
meta: { llm:
|
|
213
|
+
meta: { llm: OWASP_LLM.LLM06, call: full },
|
|
213
214
|
});
|
|
214
215
|
}
|
|
215
216
|
}
|
|
@@ -243,7 +244,7 @@ export const logicScanner = {
|
|
|
243
244
|
line: expr.loc?.start.line ?? value.loc?.start.line ?? 0,
|
|
244
245
|
cwe: 'CWE-1427: Improper Neutralization of Input Used for LLM Prompting',
|
|
245
246
|
owasp: 'A05:2025 - Injection',
|
|
246
|
-
meta: { llm:
|
|
247
|
+
meta: { llm: OWASP_LLM.LLM01, source: root },
|
|
247
248
|
});
|
|
248
249
|
return;
|
|
249
250
|
}
|
package/dist/types.d.ts
CHANGED
package/dist/utils/detect.js
CHANGED
|
@@ -56,6 +56,10 @@ export function detectFramework(root, files) {
|
|
|
56
56
|
python: exists(join(root, 'requirements.txt')) ||
|
|
57
57
|
exists(join(root, 'pyproject.toml')),
|
|
58
58
|
reactNative,
|
|
59
|
+
firebase: hasDep(pkg, 'firebase') ||
|
|
60
|
+
hasDep(pkg, 'firebase-admin') ||
|
|
61
|
+
hasDep(pkg, '@firebase/app') ||
|
|
62
|
+
exists(join(root, 'firebase.json')),
|
|
59
63
|
describe() {
|
|
60
64
|
const parts = [];
|
|
61
65
|
if (this.nextjs === 'app-router')
|
|
@@ -80,6 +84,8 @@ export function detectFramework(root, files) {
|
|
|
80
84
|
parts.push('Python');
|
|
81
85
|
if (this.reactNative)
|
|
82
86
|
parts.push('React Native');
|
|
87
|
+
if (this.firebase)
|
|
88
|
+
parts.push('Firebase');
|
|
83
89
|
return parts.length ? parts.join(' + ') : 'generic JavaScript/TypeScript';
|
|
84
90
|
},
|
|
85
91
|
};
|
package/dist/utils/owasp.d.ts
CHANGED
|
@@ -31,18 +31,28 @@ export declare const OWASP_2025: {
|
|
|
31
31
|
* would invent a mapping nobody published.
|
|
32
32
|
*/
|
|
33
33
|
export declare function normaliseOwasp(raw: string | undefined): string | null;
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* OWASP Top 10 for LLM Applications, **2026 edition** (published 4 August 2026).
|
|
36
|
+
*
|
|
37
|
+
* The renumbering is not cosmetic and it is easy to ship stale: Excessive Agency
|
|
38
|
+
* moved 06 → 03, Unbounded Consumption 10 → 06, Improper Output Handling 05 →
|
|
39
|
+
* 10, Misinformation 09 → 07, and System Prompt Leakage was renamed and
|
|
40
|
+
* broadened into **Hidden Context Exposure** at 08. Only LLM01 and LLM02 kept
|
|
41
|
+
* their numbers. Eight of ten identifiers change between editions, so anything
|
|
42
|
+
* that hard-codes them dates fast — which is why the mapping below keys off
|
|
43
|
+
* category *names*, and only the label carries the number.
|
|
44
|
+
*/
|
|
35
45
|
export declare const OWASP_LLM: {
|
|
36
|
-
readonly LLM01: 'LLM01:
|
|
37
|
-
readonly LLM02: 'LLM02:
|
|
38
|
-
readonly LLM03: 'LLM03:
|
|
39
|
-
readonly LLM04: 'LLM04:
|
|
40
|
-
readonly LLM05: 'LLM05:
|
|
41
|
-
readonly LLM06: 'LLM06:
|
|
42
|
-
readonly LLM07: 'LLM07:
|
|
43
|
-
readonly LLM08: 'LLM08:
|
|
44
|
-
readonly LLM09: 'LLM09:
|
|
45
|
-
readonly LLM10: 'LLM10:
|
|
46
|
+
readonly LLM01: 'LLM01:2026 - Prompt Injection';
|
|
47
|
+
readonly LLM02: 'LLM02:2026 - Sensitive Information Disclosure';
|
|
48
|
+
readonly LLM03: 'LLM03:2026 - Excessive Agency';
|
|
49
|
+
readonly LLM04: 'LLM04:2026 - Supply Chain';
|
|
50
|
+
readonly LLM05: 'LLM05:2026 - Data and Model Poisoning';
|
|
51
|
+
readonly LLM06: 'LLM06:2026 - Unbounded Consumption';
|
|
52
|
+
readonly LLM07: 'LLM07:2026 - Misinformation';
|
|
53
|
+
readonly LLM08: 'LLM08:2026 - Hidden Context Exposure';
|
|
54
|
+
readonly LLM09: 'LLM09:2026 - Vector and Embedding Weaknesses';
|
|
55
|
+
readonly LLM10: 'LLM10:2026 - Improper Output Handling';
|
|
46
56
|
};
|
|
47
57
|
/** The LLM category a rule's own words place it in, if any. */
|
|
48
58
|
export declare function llmCategory(text: string): string | null;
|
package/dist/utils/owasp.js
CHANGED
|
@@ -64,18 +64,28 @@ export function normaliseOwasp(raw) {
|
|
|
64
64
|
}
|
|
65
65
|
return null;
|
|
66
66
|
}
|
|
67
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* OWASP Top 10 for LLM Applications, **2026 edition** (published 4 August 2026).
|
|
69
|
+
*
|
|
70
|
+
* The renumbering is not cosmetic and it is easy to ship stale: Excessive Agency
|
|
71
|
+
* moved 06 → 03, Unbounded Consumption 10 → 06, Improper Output Handling 05 →
|
|
72
|
+
* 10, Misinformation 09 → 07, and System Prompt Leakage was renamed and
|
|
73
|
+
* broadened into **Hidden Context Exposure** at 08. Only LLM01 and LLM02 kept
|
|
74
|
+
* their numbers. Eight of ten identifiers change between editions, so anything
|
|
75
|
+
* that hard-codes them dates fast — which is why the mapping below keys off
|
|
76
|
+
* category *names*, and only the label carries the number.
|
|
77
|
+
*/
|
|
68
78
|
export const OWASP_LLM = {
|
|
69
|
-
LLM01: 'LLM01:
|
|
70
|
-
LLM02: 'LLM02:
|
|
71
|
-
LLM03: 'LLM03:
|
|
72
|
-
LLM04: 'LLM04:
|
|
73
|
-
LLM05: 'LLM05:
|
|
74
|
-
LLM06: 'LLM06:
|
|
75
|
-
LLM07: 'LLM07:
|
|
76
|
-
LLM08: 'LLM08:
|
|
77
|
-
LLM09: 'LLM09:
|
|
78
|
-
LLM10: 'LLM10:
|
|
79
|
+
LLM01: 'LLM01:2026 - Prompt Injection',
|
|
80
|
+
LLM02: 'LLM02:2026 - Sensitive Information Disclosure',
|
|
81
|
+
LLM03: 'LLM03:2026 - Excessive Agency',
|
|
82
|
+
LLM04: 'LLM04:2026 - Supply Chain',
|
|
83
|
+
LLM05: 'LLM05:2026 - Data and Model Poisoning',
|
|
84
|
+
LLM06: 'LLM06:2026 - Unbounded Consumption',
|
|
85
|
+
LLM07: 'LLM07:2026 - Misinformation',
|
|
86
|
+
LLM08: 'LLM08:2026 - Hidden Context Exposure',
|
|
87
|
+
LLM09: 'LLM09:2026 - Vector and Embedding Weaknesses',
|
|
88
|
+
LLM10: 'LLM10:2026 - Improper Output Handling',
|
|
79
89
|
};
|
|
80
90
|
/**
|
|
81
91
|
* A second, independent label for the findings that are about an LLM or agent
|
|
@@ -89,13 +99,19 @@ const LLM_RULES = [
|
|
|
89
99
|
// handling the output, even though its description discusses injection.
|
|
90
100
|
[
|
|
91
101
|
/(llm|ai|model) output[^.]{0,60}(unescaped|innerhtml|dangerouslysetinnerhtml|render|eval|exec|sink|shell|command|markdown)|(unescaped|unsanitised|unsanitized)[^.]{0,30}(llm|ai|model) output/i,
|
|
92
|
-
OWASP_LLM.
|
|
102
|
+
OWASP_LLM.LLM10,
|
|
93
103
|
],
|
|
94
104
|
[
|
|
95
105
|
/prompt injection|injected instruction|hidden instruction|jailbreak|(tool|skill) (description|definition)[^.]{0,40}(instruction|encoded|obfuscat|inject)|untrusted content into (the )?prompt/i,
|
|
96
106
|
OWASP_LLM.LLM01,
|
|
97
107
|
],
|
|
98
|
-
|
|
108
|
+
// 2026 broadened "System Prompt Leakage" into Hidden Context Exposure: the
|
|
109
|
+
// guidance is that nothing in the context window is a secret, so retrieved
|
|
110
|
+
// context and tool output belong here alongside the system prompt.
|
|
111
|
+
[
|
|
112
|
+
/(system prompt|hidden context|context window)[^.]{0,40}(leak|expos|client|bundle|browser|discoverab)/i,
|
|
113
|
+
OWASP_LLM.LLM08,
|
|
114
|
+
],
|
|
99
115
|
[
|
|
100
116
|
// A hardcoded provider key is disclosure wherever it sits, so no exposure
|
|
101
117
|
// word is required after a *named* provider. The generic "llm"/"ai" wording
|
|
@@ -103,23 +119,23 @@ const LLM_RULES = [
|
|
|
103
119
|
/(openai|anthropic|gemini|claude|mistral|cohere|huggingface|replicate|groq|perplexity|xai|pinecone)[^.]{0,40}(api[ _-]?key|token|secret)|(llm|\bai\b)[^.]{0,40}(api[ _-]?key|token|secret)[^.]{0,40}(expos|public|client|browser|bundle)|base_?url[^.]{0,40}(non-|redirect)|dangerouslyallowbrowser/i,
|
|
104
120
|
OWASP_LLM.LLM02,
|
|
105
121
|
],
|
|
106
|
-
[/(mcp|model|agent)[^.]{0,40}(@latest|unpinned|unverified|untrusted (source|registry))/i, OWASP_LLM.
|
|
122
|
+
[/(mcp|model|agent)[^.]{0,40}(@latest|unpinned|unverified|untrusted (source|registry))/i, OWASP_LLM.LLM04],
|
|
107
123
|
[
|
|
108
124
|
// "Hook" is overloaded: an npm `postinstall` hook that shells out is a
|
|
109
125
|
// supply-chain finding, not an agent given too much authority. The AI
|
|
110
126
|
// context has to be in the text.
|
|
111
127
|
/auto[- ]?approve|allowedtools|excessive agency|(mcp|agent|assistant|settings|claude|ai)[- ]?(config|hook|tool)[^.]{0,60}(execut|pipes|network|write|permissive|broad|access)|permission prompt[^.]{0,40}(bypass|skip)|overly (broad|permissive)[^.]{0,30}tool/i,
|
|
112
|
-
OWASP_LLM.
|
|
128
|
+
OWASP_LLM.LLM03,
|
|
113
129
|
],
|
|
114
130
|
// "Embedding media" is not a vector embedding — matching the bare word put a
|
|
115
131
|
// TinyMCE XSS rule in this category.
|
|
116
132
|
[
|
|
117
133
|
/vector (store|database|db|index|search)\b|\bembeddings\b|embedding (vector|model|store)|\brag\b[^.]{0,30}(poison|inject)/i,
|
|
118
|
-
OWASP_LLM.
|
|
134
|
+
OWASP_LLM.LLM09,
|
|
119
135
|
],
|
|
120
136
|
[
|
|
121
137
|
/(llm|ai|model|token)[^.]{0,40}(unbounded|no (rate|token) limit|runaway|budget)|unbounded consumption/i,
|
|
122
|
-
OWASP_LLM.
|
|
138
|
+
OWASP_LLM.LLM06,
|
|
123
139
|
],
|
|
124
140
|
];
|
|
125
141
|
/** The LLM category a rule's own words place it in, if any. */
|
package/examples/security.yml
CHANGED
|
@@ -22,7 +22,7 @@ jobs:
|
|
|
22
22
|
runs-on: ubuntu-latest
|
|
23
23
|
steps:
|
|
24
24
|
- uses: actions/checkout@v7
|
|
25
|
-
- uses: murtazaozdemir/cleartoship@v0.12.
|
|
25
|
+
- uses: murtazaozdemir/cleartoship@v0.12.2
|
|
26
26
|
with:
|
|
27
27
|
fail-on: critical # block the PR only on criticals
|
|
28
28
|
comment: true # post a summary comment on the PR
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cleartoship",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.2",
|
|
4
4
|
"description": "The 30-second pre-launch security clearance for AI-built & vibe-coded apps. Catches missing Server Action auth, Supabase RLS holes, hallucinated npm packages and leaked keys.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"security",
|