cleartoship 0.10.1 → 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
@@ -98,7 +98,7 @@ runtimes, React Native, Go and shell.
98
98
 
99
99
  27 upstream rules are **superseded** where ClearToShip's own AST check is more
100
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
101
+ **skipped as inapplicable** on a project that is not React Native, 11 carry a
102
102
  **match guard** for a
103
103
  shape their regex cannot exclude (a `"link": true` lockfile entry has no
104
104
  integrity hash by design; `querySelectorAll` is not a SQL call; `eval()` inside
@@ -218,7 +218,7 @@ jobs:
218
218
  runs-on: ubuntu-latest
219
219
  steps:
220
220
  - uses: actions/checkout@v7
221
- - uses: murtazaozdemir/cleartoship@v0.10.1
221
+ - uses: murtazaozdemir/cleartoship@v0.10.2
222
222
  with:
223
223
  fail-on: critical
224
224
  comment: true
@@ -240,7 +240,7 @@ above `fail-on`) for use in later steps. The comment is *sticky* — re-runs edi
240
240
  the same comment instead of piling up.
241
241
 
242
242
  By default the action runs the scanner version its own ref declares, so
243
- `@v0.10.1` runs `cleartoship@0.10.1` 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
244
244
  that version is not on the registry, it builds from its own checkout instead, so
245
245
  `uses: …@ref` works against an unpublished commit.
246
246
 
@@ -290,6 +290,12 @@ uploaded, and no database is connected to.
290
290
  excluded: one is a published CLI's entry point, the other is production schema.
291
291
  Across five dogfooded repos this moved 21 findings out of `critical` without
292
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.
293
299
  - **A bound parameter is not an injection.** `db.prepare(\`UPDATE ${table} SET
294
300
  csv = ? WHERE id = ?\`).bind(...)` interpolates an identifier while its values
295
301
  go through placeholders — the correct pattern, and the one a SQL-injection
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.1` runs cleartoship@0.10.1. 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.1` runs cleartoship@0.10.1 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, GUARDVIBE_REACT_NATIVE_RULE_IDS, } 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
@@ -229,6 +229,30 @@ const MATCH_GUARDS = {
229
229
  // interpolated expression may read from the request.
230
230
  VG010: (match, source, index) => sqlGuard(match, source, index),
231
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
+ },
232
256
  // SSRF is a *server* being made to fetch a URL it should not. A module marked
233
257
  // `'use client'` runs in the browser, where the request leaves the user's own
234
258
  // machine and crosses no trust boundary of yours.
@@ -245,11 +269,6 @@ const MATCH_GUARDS = {
245
269
  // query filtered to the caller's own rows does not let them. An unbounded
246
270
  // fetch of your own data is a scalability question, not a security finding.
247
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),
248
- // `description: 'eval() executes arbitrary code…'` is prose about eval, not a
249
- // call to it — and security tooling, which is a good deal of what gets
250
- // scanned, is full of that prose. Code held in a string is not code running
251
- // here; the eval that would run it is its own match, outside the quotes.
252
- VG014: (_match, source, index) => !insideStringLiteral(source, index),
253
272
  };
254
273
  /** Regexes over very large files are where catastrophic backtracking bites. */
255
274
  const MAX_BYTES = 400_000;
@@ -280,6 +299,38 @@ function inapplicable(ctx) {
280
299
  }
281
300
  return { ids, why };
282
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
+ };
283
334
  export const communityScanner = {
284
335
  name: `Community ruleset (${GUARDVIBE_RULES.length - SUPERSEDED.size - WITHHELD.size} rules)`,
285
336
  applies() {
@@ -329,12 +380,27 @@ export const communityScanner = {
329
380
  const key = `${relPath}:${line}:${rule.id}`;
330
381
  if (!seen.has(key) && !suppress.suppressed(line, rule.id)) {
331
382
  seen.add(key);
332
- 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);
333
399
  result.findings.push({
334
400
  id: rule.id,
335
401
  severity: placed.severity,
336
402
  title: rule.name,
337
- detail: rule.description + placed.note,
403
+ detail: rule.description + (adjusted?.note ?? '') + placed.note,
338
404
  fix: rule.fixCode ? `${rule.fix}\n\n${rule.fixCode}` : rule.fix,
339
405
  file: relPath,
340
406
  line,
@@ -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.1
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.1",
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",