cleartoship 0.10.0 → 0.10.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 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, 5 carry a **match guard** for a
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, 11 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.0
221
+ - uses: murtazaozdemir/cleartoship@v0.10.2
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.0` runs `cleartoship@0.10.0` and pinning the ref pins the behaviour. If
243
+ `@v0.10.2` runs `cleartoship@0.10.2` 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,25 @@ 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 CVE that only runs on a build machine is not a shipping vulnerability.**
294
+ CTS024 already split those when OSV answers; the vendored CVE rules — what
295
+ runs under `--offline` — now make the same split, dropping a match under
296
+ `devDependencies` (or a lockfile entry marked `"dev": true`) to `low` with the
297
+ reason attached. The same advisory against a dependency your users run keeps
298
+ its full severity.
299
+ - **A bound parameter is not an injection.** `db.prepare(\`UPDATE ${table} SET
300
+ csv = ? WHERE id = ?\`).bind(...)` interpolates an identifier while its values
301
+ go through placeholders — the correct pattern, and the one a SQL-injection
302
+ regex reads as the bug. Both SQL rules now read the whole statement and the
303
+ call chained to it, and stand down when the values are bound. They still fire
304
+ when any interpolation reads from the request, so a query that binds one value
305
+ and concatenates another is reported.
306
+ - **A rule that cannot apply here is not run.** The vendored ruleset covers
307
+ ground this project may not stand on: certificate pinning and WebView
308
+ hardening are React Native concerns, and a browser will not let a page pin a
309
+ certificate at all, so those rules are skipped unless the project actually is
310
+ React Native. Likewise the "no request-body size limit" rule, whose own text
311
+ says Next.js already imposes one. The report names what it skipped and why.
291
312
  - **Mass assignment means the payload arrives whole.** CTS002 and CTS043 fire when
292
313
  the object the caller sent reaches the columns — `update(body)`,
293
314
  `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.0` runs cleartoship@0.10.0. Set
32
+ action's own ref declares, so `uses: …@v0.10.2` runs cleartoship@0.10.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.10.0` runs cleartoship@0.10.0 instead of
79
+ # `uses: <owner>/cleartoship@v0.10.2` runs cleartoship@0.10.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
@@ -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, GUARDVIBE_CVE_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,15 +221,54 @@ 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
+ // The "base64 payload" test is a run of 20+ characters from the base64
233
+ // alphabet, which any long camelCase identifier satisfies:
234
+ // `description: \`${pct(clusteredAroundMedian, …)}\`` matched on the
235
+ // identifier. Interpolated expressions are code, not the description text,
236
+ // and real encoded content is not purely alphabetic.
237
+ VG881: (match) => {
238
+ const text = match.replace(/\$\{[^}]*\}/g, ' ');
239
+ if (/(?:\\x[0-9a-f]{2}){4,}|(?:\\u[0-9a-f]{4}){4,}|(?:&#\d{2,4};){4,}/i.test(text))
240
+ return true;
241
+ const run = /[A-Za-z0-9+/]{20,}={0,2}/.exec(text)?.[0];
242
+ // A slash is not evidence: "new/used/refurbished" is twenty characters of
243
+ // the base64 alphabet and a sentence. Real encoded content carries digits
244
+ // or padding.
245
+ return run !== undefined && /[0-9+=]/.test(run);
246
+ },
247
+ // `eval("require")` is the documented escape hatch for keeping a bundler from
248
+ // statically resolving a require — a constant the author typed, with no input
249
+ // reaching it. Dynamic code execution is about the dynamic part.
250
+ VG014: (match, source, index) => {
251
+ if (insideStringLiteral(source, index))
252
+ return false;
253
+ const after = source.slice(index, index + 60);
254
+ return !/^(?:eval|new\s+Function)\s*\(\s*(['"])[A-Za-z_$][\w$]*\1\s*\)/.test(after);
255
+ },
256
+ // SSRF is a *server* being made to fetch a URL it should not. A module marked
257
+ // `'use client'` runs in the browser, where the request leaves the user's own
258
+ // machine and crosses no trust boundary of yours.
259
+ VG120: (_match, source) => !/^\s*(['"])use client\1/m.test(source.slice(0, 400)),
260
+ // The name list is prefix-matched with `\w*` after it, so `hashPage === 'x'`
261
+ // and `tokenCount === 3` read as secret comparisons. A timing attack needs the
262
+ // *secret itself* on one side, so the identifier has to be one of those words,
263
+ // not merely start with one.
264
+ VG106: (match) => {
265
+ const identifier = /^[A-Za-z_$][\w$]*/.exec(match)?.[0] ?? '';
266
+ return /(secret|token|apikey|api_key|signature|hmac|hash|digest|webhook)$/i.test(identifier);
267
+ },
178
268
  // "An attacker can request the entire table" is the rule's premise, and a
179
269
  // query filtered to the caller's own rows does not let them. An unbounded
180
270
  // fetch of your own data is a scalability question, not a security finding.
181
271
  VG955: (match) => !/\bwhere\b[\s\S]{0,200}?\b(userId|user_id|ownerId|owner_id|orgId|org_id|organizationId|tenantId|tenant_id|workspaceId|workspace_id|accountId|account_id|teamId|team_id|shop|shopDomain|storeId|store_id)\b/i.test(match),
182
- // `description: 'eval() executes arbitrary code…'` is prose about eval, not a
183
- // call to it — and security tooling, which is a good deal of what gets
184
- // scanned, is full of that prose. Code held in a string is not code running
185
- // here; the eval that would run it is its own match, outside the quotes.
186
- VG014: (_match, source, index) => !insideStringLiteral(source, index),
187
272
  };
188
273
  /** Regexes over very large files are where catastrophic backtracking bites. */
189
274
  const MAX_BYTES = 400_000;
@@ -193,6 +278,59 @@ function severityOf(value) {
193
278
  ? value
194
279
  : 'medium';
195
280
  }
281
+ /**
282
+ * Rules that only apply on a platform this project is not. Skipping them is not
283
+ * a judgement about the rule — it is that the advice cannot be followed here.
284
+ */
285
+ function inapplicable(ctx) {
286
+ const ids = new Set();
287
+ const why = [];
288
+ if (!ctx.framework.reactNative) {
289
+ for (const id of GUARDVIBE_REACT_NATIVE_RULE_IDS)
290
+ ids.add(id);
291
+ why.push(`${GUARDVIBE_REACT_NATIVE_RULE_IDS.size} React Native rules (not a mobile project)`);
292
+ }
293
+ // VG132 asks for an explicit request-body size limit and says itself that
294
+ // Next.js and Vercel already impose one. On a Next.js project it is advice
295
+ // about a limit the framework has already applied.
296
+ if (ctx.framework.nextjs !== null) {
297
+ ids.add('VG132');
298
+ why.push('VG132 body-size limit (Next.js sets one by default)');
299
+ }
300
+ return { ids, why };
301
+ }
302
+ /**
303
+ * Whether a dependency-manifest match sits under `devDependencies`, or in a
304
+ * lockfile entry marked `"dev": true`. Both mean the package is a build-time
305
+ * tool that no user ever runs — the split CTS024 already makes for CVEs found
306
+ * through OSV, applied to the vendored CVE rules that run when offline.
307
+ */
308
+ function inDevDependencies(source, index) {
309
+ const before = source.slice(Math.max(0, index - 4000), index);
310
+ if (/"dev"\s*:\s*true[\s\S]{0,600}$/.test(before))
311
+ return true;
312
+ const nearest = /"(dev|peer|optional)?[dD]ependencies"\s*:\s*\{(?![\s\S]*"[a-z]*[dD]ependencies"\s*:\s*\{)/.exec(before);
313
+ return nearest?.[1] === 'dev';
314
+ }
315
+ /**
316
+ * Rules whose upstream severity is right for one shape they match and wrong for
317
+ * another. Returning null leaves the rule's own severity alone.
318
+ */
319
+ const SEVERITY_ADJUSTERS = {
320
+ // The rule matches two different things. Explicitly accepting `alg: none` is
321
+ // the critical it is named for. Merely calling `jwt.verify(token, secret)`
322
+ // without pinning `algorithms` is not: jsonwebtoken has rejected `none` on a
323
+ // keyed verify since v9, so what is left is defence against algorithm
324
+ // confusion — worth doing, not worth blocking a deploy over.
325
+ VG105: (match) => /algorithms\s*:\s*\[\s*['"]none['"]/i.test(match)
326
+ ? null
327
+ : {
328
+ severity: 'medium',
329
+ note: ' (Reported at medium: no `algorithms` option is pinned, but nothing here accepts ' +
330
+ '`alg: none` — a keyed `jwt.verify` rejects it. Pinning the algorithm is defence ' +
331
+ 'against algorithm confusion, which matters most when the key could be a public key.)',
332
+ },
333
+ };
196
334
  export const communityScanner = {
197
335
  name: `Community ruleset (${GUARDVIBE_RULES.length - SUPERSEDED.size - WITHHELD.size} rules)`,
198
336
  applies() {
@@ -200,7 +338,8 @@ export const communityScanner = {
200
338
  },
201
339
  async run(ctx) {
202
340
  const result = emptyResult();
203
- const active = GUARDVIBE_RULES.filter((r) => !SUPERSEDED.has(r.id) && !WITHHELD.has(r.id));
341
+ const platform = inapplicable(ctx);
342
+ const active = GUARDVIBE_RULES.filter((r) => !SUPERSEDED.has(r.id) && !WITHHELD.has(r.id) && !platform.ids.has(r.id));
204
343
  const seen = new Set();
205
344
  let filesScanned = 0;
206
345
  for (const file of ctx.files) {
@@ -241,12 +380,27 @@ export const communityScanner = {
241
380
  const key = `${relPath}:${line}:${rule.id}`;
242
381
  if (!seen.has(key) && !suppress.suppressed(line, rule.id)) {
243
382
  seen.add(key);
244
- const placed = adjustForPath(severityOf(rule.severity), relPath);
383
+ let adjusted = SEVERITY_ADJUSTERS[rule.id]?.(m[0], source, m.index) ?? null;
384
+ // A CVE in something that only ever runs on a build machine is not
385
+ // a shipping vulnerability. OSV-sourced findings are already split
386
+ // this way (CTS024); this is the same split for the vendored CVE
387
+ // rules, which are what runs with --offline.
388
+ if (!adjusted &&
389
+ GUARDVIBE_CVE_RULE_IDS.has(rule.id) &&
390
+ (lockfile || /(^|\/)package\.json$/.test(relPath)) &&
391
+ inDevDependencies(source, m.index)) {
392
+ adjusted = {
393
+ severity: 'low',
394
+ note: ' (Reported at low: this version is declared under devDependencies, so it is a ' +
395
+ 'build-time tool rather than something your users run.)',
396
+ };
397
+ }
398
+ const placed = adjustForPath(adjusted?.severity ?? severityOf(rule.severity), relPath);
245
399
  result.findings.push({
246
400
  id: rule.id,
247
401
  severity: placed.severity,
248
402
  title: rule.name,
249
- detail: rule.description + placed.note,
403
+ detail: rule.description + (adjusted?.note ?? '') + placed.note,
250
404
  fix: rule.fixCode ? `${rule.fix}\n\n${rule.fixCode}` : rule.fix,
251
405
  file: relPath,
252
406
  line,
@@ -271,7 +425,8 @@ export const communityScanner = {
271
425
  label: `Community ruleset (${active.length} rules over ${filesScanned} files)`,
272
426
  passed: result.findings.every((f) => f.severity !== 'critical'),
273
427
  note: `${SUPERSEDED.size} superseded by ClearToShip's AST checks, ${WITHHELD.size} withheld as noisy, ` +
274
- `${MANIFEST_ONLY.size} manifest-only (not run over lockfiles)`,
428
+ `${MANIFEST_ONLY.size} manifest-only (not run over lockfiles)` +
429
+ (platform.why.length ? `; skipped as inapplicable: ${platform.why.join(', ')}` : ''),
275
430
  });
276
431
  return result;
277
432
  },
@@ -655,7 +655,14 @@ export const serverActionsScanner = {
655
655
  meta: { kind, route: relPath },
656
656
  });
657
657
  }
658
- if (writes && info.hasAuth && !info.ownerScoped && info.params > 0) {
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
@@ -53,6 +53,7 @@ export interface FrameworkInfo {
53
53
  nextAuth: boolean;
54
54
  stripe: boolean;
55
55
  python: boolean;
56
+ reactNative: boolean;
56
57
  /** Human-readable one-liner for the CLI header. */
57
58
  describe(): string;
58
59
  }
@@ -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
  };
@@ -1,6 +1,15 @@
1
1
  import { SEVERITY_ORDER } from '../types.js';
2
- /** Tests, fixtures, examples and docs: the code is there to be read, not run. */
3
- const NON_PRODUCTION = /(^|\/)(tests?|__tests__|__mocks__|__fixtures__|fixtures?|spec|specs|examples?|docs?|demo|samples?|e2e|cypress|playwright|stories)(\/|$)|\.(test|spec|stories|fixture)\.[a-z]+$/i;
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)(\/|$)|(^|\/)[^/]*\.config\.[cm]?[jt]s$|(^|\/)(Makefile|Dockerfile[^/]*|docker-compose[^/]*\.ya?ml)$/i;
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';
@@ -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.0
25
+ - uses: murtazaozdemir/cleartoship@v0.10.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.10.0",
3
+ "version": "0.10.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",