cleartoship 0.10.0 → 0.10.1
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 +18 -3
- package/action.yml +2 -2
- package/dist/scanners/community.js +96 -7
- package/dist/scanners/server-actions.js +8 -1
- package/dist/types.d.ts +1 -0
- package/dist/utils/detect.js +10 -0
- package/dist/utils/paths.js +12 -3
- package/dist/vendor/guardvibe/index.d.ts +7 -0
- package/dist/vendor/guardvibe/index.js +7 -0
- package/examples/security.yml +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -97,7 +97,9 @@ Dockerfiles, Terraform, GitHub Actions pinning, prompt injection and MCP tool
|
|
|
97
97
|
runtimes, React Native, Go and shell.
|
|
98
98
|
|
|
99
99
|
27 upstream rules are **superseded** where ClearToShip's own AST check is more
|
|
100
|
-
precise, 6 are **withheld** as measurably noisy,
|
|
100
|
+
precise, 6 are **withheld** as measurably noisy, 10 React Native rules are
|
|
101
|
+
**skipped as inapplicable** on a project that is not React Native, 9 carry a
|
|
102
|
+
**match guard** for a
|
|
101
103
|
shape their regex cannot exclude (a `"link": true` lockfile entry has no
|
|
102
104
|
integrity hash by design; `querySelectorAll` is not a SQL call; `eval()` inside
|
|
103
105
|
a sentence about eval is prose), and 3 name-heuristic rules are
|
|
@@ -216,7 +218,7 @@ jobs:
|
|
|
216
218
|
runs-on: ubuntu-latest
|
|
217
219
|
steps:
|
|
218
220
|
- uses: actions/checkout@v7
|
|
219
|
-
- uses: murtazaozdemir/cleartoship@v0.10.
|
|
221
|
+
- uses: murtazaozdemir/cleartoship@v0.10.1
|
|
220
222
|
with:
|
|
221
223
|
fail-on: critical
|
|
222
224
|
comment: true
|
|
@@ -238,7 +240,7 @@ above `fail-on`) for use in later steps. The comment is *sticky* — re-runs edi
|
|
|
238
240
|
the same comment instead of piling up.
|
|
239
241
|
|
|
240
242
|
By default the action runs the scanner version its own ref declares, so
|
|
241
|
-
`@v0.10.
|
|
243
|
+
`@v0.10.1` runs `cleartoship@0.10.1` and pinning the ref pins the behaviour. If
|
|
242
244
|
that version is not on the registry, it builds from its own checkout instead, so
|
|
243
245
|
`uses: …@ref` works against an unpublished commit.
|
|
244
246
|
|
|
@@ -288,6 +290,19 @@ uploaded, and no database is connected to.
|
|
|
288
290
|
excluded: one is a published CLI's entry point, the other is production schema.
|
|
289
291
|
Across five dogfooded repos this moved 21 findings out of `critical` without
|
|
290
292
|
hiding one of them.
|
|
293
|
+
- **A bound parameter is not an injection.** `db.prepare(\`UPDATE ${table} SET
|
|
294
|
+
csv = ? WHERE id = ?\`).bind(...)` interpolates an identifier while its values
|
|
295
|
+
go through placeholders — the correct pattern, and the one a SQL-injection
|
|
296
|
+
regex reads as the bug. Both SQL rules now read the whole statement and the
|
|
297
|
+
call chained to it, and stand down when the values are bound. They still fire
|
|
298
|
+
when any interpolation reads from the request, so a query that binds one value
|
|
299
|
+
and concatenates another is reported.
|
|
300
|
+
- **A rule that cannot apply here is not run.** The vendored ruleset covers
|
|
301
|
+
ground this project may not stand on: certificate pinning and WebView
|
|
302
|
+
hardening are React Native concerns, and a browser will not let a page pin a
|
|
303
|
+
certificate at all, so those rules are skipped unless the project actually is
|
|
304
|
+
React Native. Likewise the "no request-body size limit" rule, whose own text
|
|
305
|
+
says Next.js already imposes one. The report names what it skipped and why.
|
|
291
306
|
- **Mass assignment means the payload arrives whole.** CTS002 and CTS043 fire when
|
|
292
307
|
the object the caller sent reaches the columns — `update(body)`,
|
|
293
308
|
`data: { ...input }`, including one level down where Prisma and Drizzle put it.
|
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.10.
|
|
32
|
+
action's own ref declares, so `uses: …@v0.10.1` runs cleartoship@0.10.1. 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.10.
|
|
79
|
+
# `uses: <owner>/cleartoship@v0.10.1` runs cleartoship@0.10.1 instead of
|
|
80
80
|
# whatever npm happens to tag `latest` at the time.
|
|
81
81
|
ver="$INPUT_VERSION"
|
|
82
82
|
if [ -z "$ver" ]; then
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { read, rel, lineAt, snippetAt, languagesFor } from '../utils/files.js';
|
|
2
2
|
import { Suppressions } from '../utils/suppress.js';
|
|
3
3
|
import { adjustForPath } from '../utils/paths.js';
|
|
4
|
-
import { GUARDVIBE_RULES, GUARDVIBE_ATTRIBUTION } from '../vendor/guardvibe/index.js';
|
|
4
|
+
import { GUARDVIBE_RULES, GUARDVIBE_ATTRIBUTION, GUARDVIBE_REACT_NATIVE_RULE_IDS, } from '../vendor/guardvibe/index.js';
|
|
5
5
|
import { emptyResult } from '../types.js';
|
|
6
6
|
/**
|
|
7
7
|
* Upstream rules that restate a check ClearToShip already performs against the
|
|
@@ -121,6 +121,56 @@ function looksLikeSecretValue(match) {
|
|
|
121
121
|
return false;
|
|
122
122
|
return true;
|
|
123
123
|
}
|
|
124
|
+
/** Bind placeholders: `?`, `$1`, `:name`, `@name`, anywhere a value may go. */
|
|
125
|
+
const BIND_PLACEHOLDER = /[\s(,=]\?(?=[\s,)`;]|$)|\$\d+\b|[\s(,=]:[A-Za-z_]\w*|[\s(,=]@[A-Za-z_]\w*/;
|
|
126
|
+
/** `${...}` expressions that read straight from the request. */
|
|
127
|
+
const INTERPOLATES_REQUEST = /\$\{[^}]*\b(req|request|body|params|query|searchParams|argv|input|formData|payload)\b/i;
|
|
128
|
+
/**
|
|
129
|
+
* The whole statement a match sits in. Both SQL rules stop matching at the first
|
|
130
|
+
* `${`, so the bind placeholders that decide the question are usually *past* the
|
|
131
|
+
* end of the match — `prepare(\`UPDATE ${table} SET csv_data = ? WHERE id = ?\`)`
|
|
132
|
+
* matches only as far as `UPDATE ${`.
|
|
133
|
+
*/
|
|
134
|
+
function statementAround(source, index) {
|
|
135
|
+
const open = source.indexOf('`', index);
|
|
136
|
+
if (open !== -1 && open - index < 120) {
|
|
137
|
+
for (let i = open + 1; i < source.length && i < open + 800; i++) {
|
|
138
|
+
if (source[i] === '\\') {
|
|
139
|
+
i++;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
// A little past the closing backtick, so the chained `.bind(...)` that
|
|
143
|
+
// decides whether the values are parameterized is inside the window.
|
|
144
|
+
if (source[i] === '`')
|
|
145
|
+
return source.slice(index, i + 121);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return source.slice(index, index + 400);
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Whether the statement passes its values as bound parameters. Interpolating a
|
|
152
|
+
* table name into an otherwise parameterized query is ordinary; interpolating
|
|
153
|
+
* `${req.query.id}` is the bug, and a query doing both still fails this test.
|
|
154
|
+
*/
|
|
155
|
+
function isParameterized(statement) {
|
|
156
|
+
if (INTERPOLATES_REQUEST.test(statement))
|
|
157
|
+
return false;
|
|
158
|
+
// Handing the statement to `.bind(...)` is the parameterizing itself, and it
|
|
159
|
+
// covers the idioms a placeholder scan cannot see: `IN (${ids.map(() =>
|
|
160
|
+
// '?').join(',')})`, or a constant SQL fragment interpolated beside binds.
|
|
161
|
+
if (/\.\s*bind\s*\(\s*[^)\s]/.test(statement))
|
|
162
|
+
return true;
|
|
163
|
+
// Otherwise look for the placeholders themselves — with interpolations
|
|
164
|
+
// dropped first, so a JavaScript ternary is not read as a `?` parameter.
|
|
165
|
+
return BIND_PLACEHOLDER.test(statement.replace(/\$\{[^}]*\}/g, ' '));
|
|
166
|
+
}
|
|
167
|
+
/** The verb has to be a SQL verb, and the statement must not be parameterized. */
|
|
168
|
+
function sqlGuard(match, source, index) {
|
|
169
|
+
const verb = /^[A-Za-z_]+/.exec(match)?.[0] ?? '';
|
|
170
|
+
if (!SQL_VERBS.test(verb) && !SQL_KEYWORDS.test(match))
|
|
171
|
+
return false;
|
|
172
|
+
return !isParameterized(statementAround(source, index));
|
|
173
|
+
}
|
|
124
174
|
/** Whether `index` falls inside a quoted string on its own line. */
|
|
125
175
|
function insideStringLiteral(source, index) {
|
|
126
176
|
const lineStart = source.lastIndexOf('\n', index - 1) + 1;
|
|
@@ -155,10 +205,6 @@ const MATCH_GUARDS = {
|
|
|
155
205
|
// SQL call. Weak verbs have to be backed by something that looks like SQL;
|
|
156
206
|
// `query`, `execute` and friends stand on their own. A real interpolated
|
|
157
207
|
// `exec(\`INSERT INTO ...\`)` still matches — checked against one.
|
|
158
|
-
VG010: (match) => {
|
|
159
|
-
const verb = /^[A-Za-z_]+/.exec(match)?.[0] ?? '';
|
|
160
|
-
return SQL_VERBS.test(verb) || SQL_KEYWORDS.test(match);
|
|
161
|
-
},
|
|
162
208
|
// `(?:child_process|cp)[\s\S]*?(?:exec|spawn…)` lets the bridge run to the end
|
|
163
209
|
// of the file: one match measured 3,608 characters and 109 lines, pairing an
|
|
164
210
|
// `import … from "node:child_process"` with an `exec(` far below it and
|
|
@@ -175,6 +221,26 @@ const MATCH_GUARDS = {
|
|
|
175
221
|
// is an opaque token, not a sentence and not a placeholder.
|
|
176
222
|
VG001: (match) => looksLikeSecretValue(match),
|
|
177
223
|
VG062: (match) => looksLikeSecretValue(match),
|
|
224
|
+
// A statement whose values go through bind placeholders is parameterized:
|
|
225
|
+
// `db.prepare(\`UPDATE ${table} SET csv_data = ? WHERE id = ?\`).bind(...)`
|
|
226
|
+
// interpolates an identifier, not user input, and both rules read that as
|
|
227
|
+
// injection. Held to two conditions, so a query that binds one value and
|
|
228
|
+
// concatenates another still fires: there must be a placeholder, and no
|
|
229
|
+
// interpolated expression may read from the request.
|
|
230
|
+
VG010: (match, source, index) => sqlGuard(match, source, index),
|
|
231
|
+
VG123: (_match, source, index) => !isParameterized(statementAround(source, index)),
|
|
232
|
+
// SSRF is a *server* being made to fetch a URL it should not. A module marked
|
|
233
|
+
// `'use client'` runs in the browser, where the request leaves the user's own
|
|
234
|
+
// machine and crosses no trust boundary of yours.
|
|
235
|
+
VG120: (_match, source) => !/^\s*(['"])use client\1/m.test(source.slice(0, 400)),
|
|
236
|
+
// The name list is prefix-matched with `\w*` after it, so `hashPage === 'x'`
|
|
237
|
+
// and `tokenCount === 3` read as secret comparisons. A timing attack needs the
|
|
238
|
+
// *secret itself* on one side, so the identifier has to be one of those words,
|
|
239
|
+
// not merely start with one.
|
|
240
|
+
VG106: (match) => {
|
|
241
|
+
const identifier = /^[A-Za-z_$][\w$]*/.exec(match)?.[0] ?? '';
|
|
242
|
+
return /(secret|token|apikey|api_key|signature|hmac|hash|digest|webhook)$/i.test(identifier);
|
|
243
|
+
},
|
|
178
244
|
// "An attacker can request the entire table" is the rule's premise, and a
|
|
179
245
|
// query filtered to the caller's own rows does not let them. An unbounded
|
|
180
246
|
// fetch of your own data is a scalability question, not a security finding.
|
|
@@ -193,6 +259,27 @@ function severityOf(value) {
|
|
|
193
259
|
? value
|
|
194
260
|
: 'medium';
|
|
195
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Rules that only apply on a platform this project is not. Skipping them is not
|
|
264
|
+
* a judgement about the rule — it is that the advice cannot be followed here.
|
|
265
|
+
*/
|
|
266
|
+
function inapplicable(ctx) {
|
|
267
|
+
const ids = new Set();
|
|
268
|
+
const why = [];
|
|
269
|
+
if (!ctx.framework.reactNative) {
|
|
270
|
+
for (const id of GUARDVIBE_REACT_NATIVE_RULE_IDS)
|
|
271
|
+
ids.add(id);
|
|
272
|
+
why.push(`${GUARDVIBE_REACT_NATIVE_RULE_IDS.size} React Native rules (not a mobile project)`);
|
|
273
|
+
}
|
|
274
|
+
// VG132 asks for an explicit request-body size limit and says itself that
|
|
275
|
+
// Next.js and Vercel already impose one. On a Next.js project it is advice
|
|
276
|
+
// about a limit the framework has already applied.
|
|
277
|
+
if (ctx.framework.nextjs !== null) {
|
|
278
|
+
ids.add('VG132');
|
|
279
|
+
why.push('VG132 body-size limit (Next.js sets one by default)');
|
|
280
|
+
}
|
|
281
|
+
return { ids, why };
|
|
282
|
+
}
|
|
196
283
|
export const communityScanner = {
|
|
197
284
|
name: `Community ruleset (${GUARDVIBE_RULES.length - SUPERSEDED.size - WITHHELD.size} rules)`,
|
|
198
285
|
applies() {
|
|
@@ -200,7 +287,8 @@ export const communityScanner = {
|
|
|
200
287
|
},
|
|
201
288
|
async run(ctx) {
|
|
202
289
|
const result = emptyResult();
|
|
203
|
-
const
|
|
290
|
+
const platform = inapplicable(ctx);
|
|
291
|
+
const active = GUARDVIBE_RULES.filter((r) => !SUPERSEDED.has(r.id) && !WITHHELD.has(r.id) && !platform.ids.has(r.id));
|
|
204
292
|
const seen = new Set();
|
|
205
293
|
let filesScanned = 0;
|
|
206
294
|
for (const file of ctx.files) {
|
|
@@ -271,7 +359,8 @@ export const communityScanner = {
|
|
|
271
359
|
label: `Community ruleset (${active.length} rules over ${filesScanned} files)`,
|
|
272
360
|
passed: result.findings.every((f) => f.severity !== 'critical'),
|
|
273
361
|
note: `${SUPERSEDED.size} superseded by ClearToShip's AST checks, ${WITHHELD.size} withheld as noisy, ` +
|
|
274
|
-
`${MANIFEST_ONLY.size} manifest-only (not run over lockfiles)
|
|
362
|
+
`${MANIFEST_ONLY.size} manifest-only (not run over lockfiles)` +
|
|
363
|
+
(platform.why.length ? `; skipped as inapplicable: ${platform.why.join(', ')}` : ''),
|
|
275
364
|
});
|
|
276
365
|
return result;
|
|
277
366
|
},
|
|
@@ -655,7 +655,14 @@ export const serverActionsScanner = {
|
|
|
655
655
|
meta: { kind, route: relPath },
|
|
656
656
|
});
|
|
657
657
|
}
|
|
658
|
-
|
|
658
|
+
// A Route Handler always has a `request` parameter, so "takes an
|
|
659
|
+
// argument" says nothing about it — `POST /api/auth/logout`, which
|
|
660
|
+
// resolves the session and destroys it, was reported as an IDOR. What
|
|
661
|
+
// the rule needs is a real write keyed on something the caller sent.
|
|
662
|
+
const idorShaped = isRoute
|
|
663
|
+
? info.hasMutation && info.readsRequestInput
|
|
664
|
+
: info.params > 0;
|
|
665
|
+
if (writes && info.hasAuth && !info.ownerScoped && idorShaped) {
|
|
659
666
|
push({
|
|
660
667
|
id: 'CTS004',
|
|
661
668
|
severity: 'medium',
|
package/dist/types.d.ts
CHANGED
package/dist/utils/detect.js
CHANGED
|
@@ -38,6 +38,13 @@ export function detectFramework(root, files) {
|
|
|
38
38
|
hasDep(pkg, '@supabase/ssr') ||
|
|
39
39
|
hasDep(pkg, '@supabase/auth-helpers-nextjs') ||
|
|
40
40
|
exists(join(root, 'supabase'));
|
|
41
|
+
// Expo counts: it is React Native underneath, and the mobile rules apply.
|
|
42
|
+
const reactNative = hasDep(pkg, 'react-native') ||
|
|
43
|
+
hasDep(pkg, 'expo') ||
|
|
44
|
+
exists(join(root, 'metro.config.js')) ||
|
|
45
|
+
exists(join(root, 'metro.config.ts')) ||
|
|
46
|
+
exists(join(root, 'app.json')) &&
|
|
47
|
+
/\b(expo|react-native)\b/.test(read(join(root, 'app.json')) ?? '');
|
|
41
48
|
const info = {
|
|
42
49
|
nextjs,
|
|
43
50
|
supabase,
|
|
@@ -48,6 +55,7 @@ export function detectFramework(root, files) {
|
|
|
48
55
|
stripe: hasDep(pkg, 'stripe'),
|
|
49
56
|
python: exists(join(root, 'requirements.txt')) ||
|
|
50
57
|
exists(join(root, 'pyproject.toml')),
|
|
58
|
+
reactNative,
|
|
51
59
|
describe() {
|
|
52
60
|
const parts = [];
|
|
53
61
|
if (this.nextjs === 'app-router')
|
|
@@ -70,6 +78,8 @@ export function detectFramework(root, files) {
|
|
|
70
78
|
parts.push('Stripe');
|
|
71
79
|
if (this.python)
|
|
72
80
|
parts.push('Python');
|
|
81
|
+
if (this.reactNative)
|
|
82
|
+
parts.push('React Native');
|
|
73
83
|
return parts.length ? parts.join(' + ') : 'generic JavaScript/TypeScript';
|
|
74
84
|
},
|
|
75
85
|
};
|
package/dist/utils/paths.js
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { SEVERITY_ORDER } from '../types.js';
|
|
2
|
-
/**
|
|
3
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Tests, fixtures, examples and docs: the code is there to be read, not run.
|
|
4
|
+
*
|
|
5
|
+
* The test family accepts an affix — `transfer-tests/`, `integration_test/` —
|
|
6
|
+
* because that is how those directories get named. The rest are matched whole
|
|
7
|
+
* on purpose: `demo` is a fixture directory, but `demo-billing/` may well be a
|
|
8
|
+
* shipped feature, and quietly downgrading it would be the worse mistake.
|
|
9
|
+
* Every segment must be followed by a slash — these are directories. A file
|
|
10
|
+
* named `spec-parser.ts` is production code that happens to parse specs.
|
|
11
|
+
*/
|
|
12
|
+
const NON_PRODUCTION = /(^|\/)(?:(?:[\w.]+[-_])?(?:tests?|specs?|e2e|fixtures?|mocks?)(?:[-_][\w.]+)?|__tests__|__mocks__|__fixtures__|examples?|docs?|demo|samples?|cypress|playwright|stories)\/|\.(test|spec|stories|fixture)\.[a-z]+$/i;
|
|
4
13
|
/**
|
|
5
14
|
* Build and maintenance tooling. It runs on a developer's machine or in CI,
|
|
6
15
|
* with credentials its author already holds, and never answers a request from
|
|
@@ -10,7 +19,7 @@ const NON_PRODUCTION = /(^|\/)(tests?|__tests__|__mocks__|__fixtures__|fixtures?
|
|
|
10
19
|
* `bin/` is deliberately absent: for a published CLI that is the shipped entry
|
|
11
20
|
* point. So is `migrations/`, which is the production schema.
|
|
12
21
|
*/
|
|
13
|
-
const DEV_TOOLING = /(^|\/)(scripts?|tools?|tooling|\.github|\.husky|\.circleci|seeds?|benchmarks?|codegen)(
|
|
22
|
+
const DEV_TOOLING = /(^|\/)(scripts?|tools?|tooling|\.github|\.husky|\.circleci|seeds?|benchmarks?|codegen)\/|(^|\/)[^/]*\.config\.[cm]?[jt]s$|(^|\/)(Makefile|Dockerfile[^/]*|docker-compose[^/]*\.ya?ml)$/i;
|
|
14
23
|
export function pathClass(relPath) {
|
|
15
24
|
if (NON_PRODUCTION.test(relPath))
|
|
16
25
|
return 'non-production';
|
|
@@ -8,4 +8,11 @@ export declare const GUARDVIBE_RULES: SecurityRule[];
|
|
|
8
8
|
* running with --offline.
|
|
9
9
|
*/
|
|
10
10
|
export declare const GUARDVIBE_CVE_RULE_IDS: ReadonlySet<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Ids of the React Native ruleset. Certificate pinning, WebView hardening and
|
|
13
|
+
* AsyncStorage hygiene are mobile concerns; on a web or server project they are
|
|
14
|
+
* not weaker advice, they are inapplicable — a browser will not let a page pin a
|
|
15
|
+
* certificate at all. Skipped unless the project actually is React Native.
|
|
16
|
+
*/
|
|
17
|
+
export declare const GUARDVIBE_REACT_NATIVE_RULE_IDS: ReadonlySet<string>;
|
|
11
18
|
export declare const GUARDVIBE_ATTRIBUTION = "GuardVibe (github.com/goklab/guardvibe), Copyright 2026 GokLab, Apache-2.0";
|
|
@@ -57,4 +57,11 @@ export const GUARDVIBE_RULES = [
|
|
|
57
57
|
* running with --offline.
|
|
58
58
|
*/
|
|
59
59
|
export const GUARDVIBE_CVE_RULE_IDS = new Set(cveVersionRules.map((r) => r.id));
|
|
60
|
+
/**
|
|
61
|
+
* Ids of the React Native ruleset. Certificate pinning, WebView hardening and
|
|
62
|
+
* AsyncStorage hygiene are mobile concerns; on a web or server project they are
|
|
63
|
+
* not weaker advice, they are inapplicable — a browser will not let a page pin a
|
|
64
|
+
* certificate at all. Skipped unless the project actually is React Native.
|
|
65
|
+
*/
|
|
66
|
+
export const GUARDVIBE_REACT_NATIVE_RULE_IDS = new Set(reactNativeRules.map((r) => r.id));
|
|
60
67
|
export const GUARDVIBE_ATTRIBUTION = 'GuardVibe (github.com/goklab/guardvibe), Copyright 2026 GokLab, Apache-2.0';
|
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.10.
|
|
25
|
+
- uses: murtazaozdemir/cleartoship@v0.10.1
|
|
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.10.
|
|
3
|
+
"version": "0.10.1",
|
|
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",
|