Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 59 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
"lint:ci": "eslint --color --ext .js,.jsx,.ts,.tsx src",
"lint:check": "eslint --quiet --ext .js,.jsx,.ts,.tsx src",
"lint:fix": "eslint --fix --ext .js,.jsx,.ts,.tsx src",
"prepare": "husky install"
"prepare": "husky install",
"audit:osv": "node scripts/check-vulns.js --level=high"
},
"dependencies": {
"@craco/craco": "^7.1.0",
Expand Down Expand Up @@ -122,16 +123,71 @@
"typescript": "^5.0.4"
},
"//resolutions": "wagmi/viem are pinned because @taquito 24.x's nested @noble/curves needs @noble/hashes 1.x (pinned below), while current viem (via ox) needs @noble/hashes 2.x. The two ecosystems have incompatible @noble/hashes requirements. Revisit once @taquito ships a build that works against @noble/hashes 2.x.",
"//resolutions-osv": "OSV high/critical audit (scripts/check-vulns.js). Same-major bumps are blanket; packages installed under multiple majors are scoped per parent so each consumer keeps its major. path-to-regexp 2.2.1 (serve-handler, via `serve`) has no 2.x fix; bumped serve-handler's copy across the major to 3.3.0 (verified serve-handler still serves correctly against it) since serve-handler's usage is a trivial default-export call + .compile().",
"resolutions": {
"@types/react": "~17.0.3",
"react-error-overlay": "6.0.9",
"viem": "2.17.4",
"wagmi": "2.10.10",
"mipd": "0.0.7",
"protobufjs": "7.5.8",
"protobufjs": "7.6.3",
"@noble/hashes": "1.8.0",
"dompurify": "3.4.3",
"shell-quote": "1.8.4"
"shell-quote": "1.8.4",
"@babel/plugin-transform-modules-systemjs": "7.29.4",
"@coinbase/wallet-sdk": "4.3.0",
"@grpc/grpc-js": "1.9.16",
"defu": "6.1.5",
"fast-uri": "3.1.2",
"flatted": "3.4.2",
"form-data": "3.0.5",
"h3": "1.15.6",
"jsonpath": "1.3.0",
"lodash": "4.18.1",
"lodash-es": "4.18.1",
"node-forge": "1.4.0",
"rollup": "2.80.0",
"socket.io-parser": "4.2.6",
"tmp": "0.2.7",
"underscore": "1.13.8",
"websocket-driver": "0.7.5",
"ws": "8.21.0",
"**/svgo/css-select/nth-check": "2.0.1",
"**/postcss-svgo/svgo": "2.8.1",
"**/@microsoft/signalr/ws": "6.2.4",
"**/@walletconnect/sign-client/**/ws": "7.5.11",
"**/jest-environment-jsdom/**/ws": "7.5.11",
"**/serve-handler/minimatch": "3.1.4",
"**/eslint/**/minimatch": "3.1.4",
"**/jest-config/**/minimatch": "3.1.4",
"**/react-dev-utils/**/minimatch": "3.1.4",
"**/test-exclude/minimatch": "3.1.4",
"**/eslint-plugin-import/minimatch": "3.1.4",
"**/eslint-plugin-jsx-a11y/minimatch": "3.1.4",
"**/filelist/minimatch": "5.1.8",
"**/css-minimizer-webpack-plugin/serialize-javascript": "7.0.5",
"**/terser-webpack-plugin/serialize-javascript": "7.0.5",
"**/rollup-plugin-terser/serialize-javascript": "7.0.5",
"**/anymatch/picomatch": "2.3.2",
"**/jest-util/picomatch": "2.3.2",
"**/micromatch/picomatch": "2.3.2",
"**/readdirp/picomatch": "2.3.2",
"**/babel-plugin-styled-components/picomatch": "2.3.2",
"**/rollup-plugin-visualizer/picomatch": "4.0.4",
"**/tinyglobby/picomatch": "4.0.4",
"**/@rollup/pluginutils/picomatch": "2.3.2",
"**/sane/braces": "3.0.3",
"**/sane/**/braces": "3.0.3",
"**/@craco/craco/cross-spawn": "7.0.6",
"**/eslint/cross-spawn": "7.0.6",
"**/lint-staged/**/cross-spawn": "7.0.6",
"**/patch-package/cross-spawn": "7.0.6",
"**/react-dev-utils/cross-spawn": "7.0.6",
"**/clipboardy/**/cross-spawn": "6.0.6",
"**/term-size/**/cross-spawn": "6.0.6",
"**/sane/**/cross-spawn": "6.0.6",
"**/express/path-to-regexp": "0.1.13",
"**/serve-handler/path-to-regexp": "3.3.0"
},
"overrides": {
"viem": "2.17.4",
Expand Down
183 changes: 183 additions & 0 deletions scripts/check-vulns.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
#!/usr/bin/env node
/**
* Deterministic dependency vulnerability check against OSV.dev.
*
* Parses yarn.lock (v1) and cross-references every locked package@version
* against the OSV database (https://osv.dev), which aggregates GitHub
* Security Advisories / CVEs for the npm ecosystem. Replaces `yarn audit`,
* whose registry endpoint was retired.
*
* Usage:
* node scripts/check-vulns.js [--level=low|moderate|high|critical] [--json]
*
* Exits 1 if any vulnerability at or above --level (default: high) is found.
*/
const fs = require("fs")
const path = require("path")

const OSV_BATCH_URL = "https://api.osv.dev/v1/querybatch"
const OSV_VULN_URL = "https://api.osv.dev/v1/vulns/"
const BATCH_SIZE = 1000
const SEVERITY_ORDER = { low: 0, moderate: 1, high: 2, critical: 3 }

function parseArgs() {
const args = { level: "high", json: false }
for (const a of process.argv.slice(2)) {
if (a.startsWith("--level=")) args.level = a.split("=")[1].toLowerCase()
else if (a === "--json") args.json = true
}
if (!(args.level in SEVERITY_ORDER)) {
console.error(`Unknown --level "${args.level}" (use low|moderate|high|critical)`)
process.exit(2)
}
return args
}

/** Parse yarn.lock v1 into a unique set of { name, version }. */
function parseYarnLock(lockPath) {
const text = fs.readFileSync(lockPath, "utf8")
const packages = new Map()
let currentNames = null
for (const line of text.split("\n")) {
if (line.startsWith("#") || line.trim() === "") continue
if (!line.startsWith(" ")) {
// Header line: one or more comma-separated "name@range" specs (possibly quoted)
currentNames = new Set(
line
.replace(/:\s*$/, "")
.split(",")
.map(spec => {
const s = spec.trim().replace(/^"|"$/g, "")
return s.slice(0, s.lastIndexOf("@")) // strip range, keep scoped names intact
})
)
} else if (currentNames && line.trim().startsWith("version ")) {
const version = line.trim().slice("version ".length).replace(/"/g, "")
for (const name of currentNames) {
packages.set(`${name}@${version}`, { name, version })
}
}
}
return [...packages.values()]
}

/** Map OSV severity info to npm-style levels. */
function severityLevel(vuln) {
const dbSeverity = (vuln.database_specific && vuln.database_specific.severity) || ""
if (dbSeverity) {
const s = dbSeverity.toLowerCase()
if (s in SEVERITY_ORDER) return s
if (s === "medium") return "moderate"
}
const cvss = (vuln.severity || []).find(s => s.type.startsWith("CVSS"))
if (cvss) {
const score = parseFloat(String(cvss.score).match(/[\d.]+$/) ? cvss.score : "") // score may be a vector string
if (!isNaN(score)) {
if (score >= 9) return "critical"
if (score >= 7) return "high"
if (score >= 4) return "moderate"
return "low"
}
}
return "moderate" // unknown severity: don't silently downgrade to low
}

function fixedVersions(vuln, pkgName) {
const fixes = new Set()
for (const affected of vuln.affected || []) {
if (affected.package && affected.package.name !== pkgName) continue
for (const range of affected.ranges || []) {
for (const event of range.events || []) {
if (event.fixed) fixes.add(event.fixed)
}
}
}
return [...fixes]
}

async function postBatch(queries) {
const res = await fetch(OSV_BATCH_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ queries })
})
if (!res.ok) throw new Error(`OSV batch query failed: HTTP ${res.status}`)
return res.json()
}

async function getVuln(id) {
const res = await fetch(OSV_VULN_URL + id)
if (!res.ok) throw new Error(`OSV vuln fetch failed for ${id}: HTTP ${res.status}`)
return res.json()
}

async function main() {
const args = parseArgs()
const lockPath = path.join(__dirname, "..", "yarn.lock")
const packages = parseYarnLock(lockPath)
if (!args.json) console.error(`Checking ${packages.length} locked packages against OSV.dev ...`)

// Batch-query OSV: returns vuln IDs per package
const hits = [] // { name, version, ids: [] }
for (let i = 0; i < packages.length; i += BATCH_SIZE) {
const chunk = packages.slice(i, i + BATCH_SIZE)
const { results } = await postBatch(
chunk.map(p => ({ package: { name: p.name, ecosystem: "npm" }, version: p.version }))
)
results.forEach((r, idx) => {
if (r.vulns && r.vulns.length) {
hits.push({ ...chunk[idx], ids: r.vulns.map(v => v.id) })
}
})
}

// Fetch details once per unique vuln id (severity, summary, fixed range)
const uniqueIds = [...new Set(hits.flatMap(h => h.ids))]
const details = new Map()
for (const id of uniqueIds) details.set(id, await getVuln(id))

const findings = hits.map(h => ({
package: h.name,
version: h.version,
vulns: h.ids.map(id => {
const v = details.get(id)
return {
id,
aliases: v.aliases || [],
severity: severityLevel(v),
summary: v.summary || (v.details || "").split("\n")[0],
fixed: fixedVersions(v, h.name)
}
})
}))

const threshold = SEVERITY_ORDER[args.level]
const actionable = findings
.map(f => ({ ...f, vulns: f.vulns.filter(v => SEVERITY_ORDER[v.severity] >= threshold) }))
.filter(f => f.vulns.length)

if (args.json) {
console.log(JSON.stringify({ checked: packages.length, findings, actionable }, null, 2))
} else {
for (const f of findings) {
for (const v of f.vulns) {
const flag = SEVERITY_ORDER[v.severity] >= threshold ? "!!" : " "
console.log(
`${flag} [${v.severity.toUpperCase().padEnd(8)}] ${f.package}@${f.version} — ${v.id}` +
`${v.aliases.length ? ` (${v.aliases.join(", ")})` : ""}` +
`${v.fixed.length ? ` fixed in: ${v.fixed.join(", ")}` : ""}\n ${v.summary}`
)
}
}
console.log(
`\n${findings.length} vulnerable package version(s), ` +
`${actionable.length} at or above "${args.level}".`
)
}
process.exit(actionable.length ? 1 : 0)
}

main().catch(err => {
console.error(err.message)
process.exit(2)
})
Loading
Loading