Files
orca/config/scripts/dependency-security-audit.mjs
T
Neil 2078db93ec ci: check dependency vulnerabilities on PRs and stable releases (#31858)
* ci: audit dependency security without gating releases

* ci: audit stable release tags instead of running daily

* ci: isolate release audit data and fix integration checks

* ci: require exact audit inputs and ignore candidate archive attributes
2026-10-22 00:43:07 -07:00

108 lines
3.6 KiB
JavaScript

import { createHash } from 'node:crypto'
import { readFileSync } from 'node:fs'
import { join } from 'node:path'
import { parse, parseAllDocuments } from 'yaml'
import { z } from 'zod'
export const AUDIT_WORKSPACES = ['.', 'mobile', 'cloud', 'docs/site', 'config/audit-tools']
const text = z.string().trim().min(1)
const severities = ['info', 'low', 'moderate', 'high', 'critical']
export const securityExceptions = z.array(
z.strictObject({
workspace: z.enum(AUDIT_WORKSPACES),
package: text,
version: text,
advisory: z.string().regex(/^GHSA-[-a-z0-9]+$/),
owner: text,
reason: text,
expires: z.iso.date(),
patch: z.strictObject({ path: text, sha256: z.string().regex(/^[a-f0-9]{64}$/) })
})
)
const auditSchema = z.object({
advisories: z.record(
text,
z.object({
module_name: text,
github_advisory_id: text,
severity: z.enum(severities),
url: z.url(),
findings: z.array(z.object({ version: text, paths: z.array(text).min(1) })).min(1)
})
),
metadata: z.object({
vulnerabilities: z.object(
Object.fromEntries(severities.map((level) => [level, z.int().min(0)]))
)
})
})
function verifyPatch(root, exception) {
const cwd = join(root, exception.workspace)
const specifier = `${exception.package}@${exception.version}`
const policy = parse(readFileSync(join(cwd, 'pnpm-workspace.yaml'), 'utf8'))
const documents = parseAllDocuments(readFileSync(join(cwd, 'pnpm-lock.yaml'), 'utf8'))
for (const document of documents) {
if (document.errors.length) {
throw document.errors[0]
}
}
const lock = documents
.map((document) => document.toJS())
.find((value) => value?.patchedDependencies?.[specifier])
const snapshots = Object.keys(lock?.snapshots ?? {}).filter(
(key) => key === specifier || key.startsWith(`${specifier}(`)
)
if (
policy.patchedDependencies?.[specifier] !== exception.patch.path ||
lock?.patchedDependencies?.[specifier] !== exception.patch.sha256 ||
snapshots.length === 0 ||
snapshots.some((key) => !key.includes(`(patch_hash=${exception.patch.sha256})`)) ||
createHash('sha256')
.update(readFileSync(join(cwd, exception.patch.path)))
.digest('hex') !== exception.patch.sha256
) {
throw new Error(`Reviewed patch is not locked: ${exception.workspace} ${specifier}`)
}
}
export function evaluateSecurityAudit(
value,
workspace,
exceptions,
{ root, today = new Date().toISOString().slice(0, 10) } = {}
) {
const audit = auditSchema.parse(value)
const count = Object.values(audit.metadata.vulnerabilities).reduce((sum, n) => sum + n, 0)
if (count !== Object.keys(audit.advisories).length) {
throw new Error('Incomplete vulnerability report')
}
const findings = []
for (const advisory of Object.values(audit.advisories)) {
for (const finding of advisory.findings) {
const item = {
package: advisory.module_name,
version: finding.version,
advisory: advisory.github_advisory_id,
severity: advisory.severity,
url: advisory.url,
blocking: ['high', 'critical'].includes(advisory.severity)
}
const exception = exceptions.find(
(entry) =>
entry.workspace === workspace &&
entry.package === item.package &&
entry.version === item.version &&
entry.advisory === item.advisory
)
if (item.blocking && exception && exception.expires > today) {
verifyPatch(root, exception)
item.exception = exception
item.blocking = false
}
findings.push(item)
}
}
return findings
}