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 +9 -3
- package/action.yml +2 -2
- package/dist/scanners/community.js +74 -8
- package/examples/security.yml +1 -1
- package/package.json +1 -1
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,
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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,
|
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.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.
|
|
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",
|