Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
962
scripts/pre-commit/pnpm-audit-prod.mjs
Normal file
962
scripts/pre-commit/pnpm-audit-prod.mjs
Normal file
@@ -0,0 +1,962 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
// Production dependency audit helper using pnpm lock data and npm bulk advisories.
|
||||
import { readFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import process from "node:process";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { readBoundedResponseText as readBoundedResponseTextWithLimit } from "../lib/bounded-response.mjs";
|
||||
|
||||
const DEFAULT_REGISTRY = "https://registry.npmjs.org";
|
||||
const BULK_ADVISORY_PATH = "/-/npm/v1/security/advisories/bulk";
|
||||
const MIN_SEVERITY = "high";
|
||||
/** Maximum advisory error body characters retained in messages. */
|
||||
export const BULK_ADVISORY_ERROR_BODY_MAX_CHARS = 4096;
|
||||
export const BULK_ADVISORY_RESPONSE_BODY_MAX_BYTES = 8 * 1024 * 1024;
|
||||
export const BULK_ADVISORY_REQUEST_TIMEOUT_MS = 60_000;
|
||||
const MAX_TIMER_TIMEOUT_MS = 2_147_000_000;
|
||||
const SEVERITY_RANK = {
|
||||
info: 0,
|
||||
low: 1,
|
||||
moderate: 2,
|
||||
high: 3,
|
||||
critical: 4,
|
||||
};
|
||||
const TOP_LEVEL_INDENT = 0;
|
||||
const SECTION_ENTRY_INDENT = 2;
|
||||
const NESTED_SECTION_INDENT = 4;
|
||||
const MAPPING_ENTRY_INDENT = 6;
|
||||
const NESTED_MAPPING_ENTRY_INDENT = 8;
|
||||
const SNAPSHOT_SECTIONS = ["dependencies", "optionalDependencies"];
|
||||
const IMPORTER_SECTIONS = ["dependencies", "optionalDependencies"];
|
||||
const LOCAL_REFERENCE_PREFIXES = ["file:", "link:", "portal:", "workspace:"];
|
||||
// GitHub's GHSA-3q49-cfcf-g5fm feed includes an overbroad ">=0" range alongside
|
||||
// the compromised @mistralai/mistralai versions. Keep the production audit
|
||||
// blocking for the compromised releases while allowing pinned safe locks.
|
||||
const AUDIT_ADVISORY_VERSION_OVERRIDES = [
|
||||
{
|
||||
packageName: "@mistralai/mistralai",
|
||||
advisoryIds: new Set(["1118204", "GHSA-3q49-cfcf-g5fm"]),
|
||||
unaffectedVersions: new Set(["2.2.1", "2.2.5"]),
|
||||
},
|
||||
];
|
||||
|
||||
export function normalizeAuditLevel(level) {
|
||||
const normalized = String(level ?? "").toLowerCase();
|
||||
if (normalized in SEVERITY_RANK) {
|
||||
return normalized;
|
||||
}
|
||||
throw new Error(
|
||||
`Unsupported audit level "${String(level)}". Expected one of: ${Object.keys(SEVERITY_RANK).join(", ")}`,
|
||||
);
|
||||
}
|
||||
|
||||
export function stripVersionDecorators(reference) {
|
||||
const openParenIndex = reference.indexOf("(");
|
||||
if (openParenIndex === -1) {
|
||||
return reference;
|
||||
}
|
||||
return reference.slice(0, openParenIndex);
|
||||
}
|
||||
|
||||
export function parseSnapshotKey(snapshotKey) {
|
||||
let separatorIndex = -1;
|
||||
let parenDepth = 0;
|
||||
for (let index = 1; index < snapshotKey.length; index += 1) {
|
||||
const character = snapshotKey[index];
|
||||
if (character === "(") {
|
||||
parenDepth += 1;
|
||||
continue;
|
||||
}
|
||||
if (character === ")") {
|
||||
parenDepth = Math.max(0, parenDepth - 1);
|
||||
continue;
|
||||
}
|
||||
if (character === "@" && parenDepth === 0) {
|
||||
separatorIndex = index;
|
||||
}
|
||||
}
|
||||
if (separatorIndex <= 0) {
|
||||
throw new Error(`Unable to parse pnpm snapshot key "${snapshotKey}".`);
|
||||
}
|
||||
const packageName = snapshotKey.slice(0, separatorIndex);
|
||||
const reference = snapshotKey.slice(separatorIndex + 1);
|
||||
return {
|
||||
packageName,
|
||||
reference,
|
||||
version: stripVersionDecorators(reference),
|
||||
};
|
||||
}
|
||||
|
||||
function isLocalReference(reference) {
|
||||
return LOCAL_REFERENCE_PREFIXES.some((prefix) => reference.startsWith(prefix));
|
||||
}
|
||||
|
||||
function countIndentation(line) {
|
||||
let indentation = 0;
|
||||
while (indentation < line.length && line[indentation] === " ") {
|
||||
indentation += 1;
|
||||
}
|
||||
return indentation;
|
||||
}
|
||||
|
||||
function isIgnorableYamlLine(trimmed) {
|
||||
return !trimmed || trimmed.startsWith("#");
|
||||
}
|
||||
|
||||
function unquoteYamlString(value) {
|
||||
if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
|
||||
return value.slice(1, -1).replaceAll("''", "'");
|
||||
}
|
||||
if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
|
||||
return value.slice(1, -1).replaceAll('\\"', '"');
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function parseYamlScalar(value) {
|
||||
return unquoteYamlString(value.trim());
|
||||
}
|
||||
|
||||
function splitInlineYamlMapEntries(text) {
|
||||
const entries = [];
|
||||
let current = "";
|
||||
let quote = null;
|
||||
let depth = 0;
|
||||
|
||||
for (const character of text) {
|
||||
if (quote) {
|
||||
current += character;
|
||||
if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (character === "'" || character === '"') {
|
||||
quote = character;
|
||||
current += character;
|
||||
continue;
|
||||
}
|
||||
if (character === "{" || character === "[" || character === "(") {
|
||||
depth += 1;
|
||||
current += character;
|
||||
continue;
|
||||
}
|
||||
if (character === "}" || character === "]" || character === ")") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
current += character;
|
||||
continue;
|
||||
}
|
||||
if (character === "," && depth === 0) {
|
||||
const entry = current.trim();
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
current = "";
|
||||
continue;
|
||||
}
|
||||
current += character;
|
||||
}
|
||||
|
||||
const entry = current.trim();
|
||||
if (entry) {
|
||||
entries.push(entry);
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function parseInlineYamlMap(rawValue) {
|
||||
const trimmed = rawValue.trim();
|
||||
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const body = trimmed.slice(1, -1).trim();
|
||||
if (!body) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const result = {};
|
||||
for (const entry of splitInlineYamlMapEntries(body)) {
|
||||
const mapping = parseYamlMappingLine(entry);
|
||||
if (!mapping?.value) {
|
||||
continue;
|
||||
}
|
||||
result[mapping.key] = parseYamlScalar(mapping.value);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function findYamlMappingSeparator(line) {
|
||||
let quote = null;
|
||||
let depth = 0;
|
||||
|
||||
for (let index = 0; index < line.length; index += 1) {
|
||||
const character = line[index];
|
||||
if (quote) {
|
||||
if (character === quote) {
|
||||
quote = null;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (character === "'" || character === '"') {
|
||||
quote = character;
|
||||
continue;
|
||||
}
|
||||
if (character === "{" || character === "[" || character === "(") {
|
||||
depth += 1;
|
||||
continue;
|
||||
}
|
||||
if (character === "}" || character === "]" || character === ")") {
|
||||
depth = Math.max(0, depth - 1);
|
||||
continue;
|
||||
}
|
||||
if (character !== ":" || depth !== 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextCharacter = line[index + 1];
|
||||
if (nextCharacter === undefined || /\s/u.test(nextCharacter)) {
|
||||
return index;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
function parseYamlMappingLine(line) {
|
||||
const separatorIndex = findYamlMappingSeparator(line);
|
||||
if (separatorIndex === -1) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
key: parseYamlScalar(line.slice(0, separatorIndex)),
|
||||
value: line.slice(separatorIndex + 1).trim(),
|
||||
};
|
||||
}
|
||||
|
||||
function isNamedYamlSection(trimmed, sectionNames) {
|
||||
return sectionNames.some((sectionName) => trimmed === `${sectionName}:`);
|
||||
}
|
||||
|
||||
function readNestedVersionValue(lines, startIndex, parentIndent) {
|
||||
let index = startIndex;
|
||||
let version = null;
|
||||
|
||||
while (index < lines.length) {
|
||||
const nestedLine = lines[index];
|
||||
const nestedTrimmed = nestedLine.trim();
|
||||
const nestedIndentation = countIndentation(nestedLine);
|
||||
if (isIgnorableYamlLine(nestedTrimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (nestedIndentation <= parentIndent) {
|
||||
break;
|
||||
}
|
||||
if (nestedIndentation === NESTED_MAPPING_ENTRY_INDENT) {
|
||||
const nestedEntry = parseYamlMappingLine(nestedTrimmed);
|
||||
if (nestedEntry?.key === "version") {
|
||||
version = parseYamlScalar(nestedEntry.value);
|
||||
}
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return { nextIndex: index, version };
|
||||
}
|
||||
|
||||
function collectIndentedStringMap(lines, startIndex, entryIndent) {
|
||||
const entries = {};
|
||||
let index = startIndex;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index];
|
||||
const trimmed = line.trim();
|
||||
const indentation = countIndentation(line);
|
||||
|
||||
if (isIgnorableYamlLine(trimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (indentation < entryIndent) {
|
||||
break;
|
||||
}
|
||||
if (indentation !== entryIndent) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = parseYamlMappingLine(trimmed);
|
||||
if (entry?.value) {
|
||||
entries[entry.key] = parseYamlScalar(entry.value);
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return { entries, nextIndex: index };
|
||||
}
|
||||
|
||||
function collectImporterDependencyReferences(lines, startIndex) {
|
||||
const references = [];
|
||||
let index = startIndex;
|
||||
|
||||
while (index < lines.length) {
|
||||
const line = lines[index];
|
||||
const trimmed = line.trim();
|
||||
const indentation = countIndentation(line);
|
||||
|
||||
if (isIgnorableYamlLine(trimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (indentation < MAPPING_ENTRY_INDENT) {
|
||||
break;
|
||||
}
|
||||
if (indentation > MAPPING_ENTRY_INDENT) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const entry = parseYamlMappingLine(trimmed);
|
||||
index += 1;
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.value) {
|
||||
const inlineMap = parseInlineYamlMap(entry.value);
|
||||
if (inlineMap && typeof inlineMap.version === "string") {
|
||||
references.push({ dependencyName: entry.key, reference: inlineMap.version });
|
||||
continue;
|
||||
}
|
||||
references.push({ dependencyName: entry.key, reference: parseYamlScalar(entry.value) });
|
||||
continue;
|
||||
}
|
||||
|
||||
const nestedVersion = readNestedVersionValue(lines, index, MAPPING_ENTRY_INDENT);
|
||||
index = nestedVersion.nextIndex;
|
||||
if (nestedVersion.version) {
|
||||
references.push({ dependencyName: entry.key, reference: nestedVersion.version });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
nextIndex: index,
|
||||
references,
|
||||
};
|
||||
}
|
||||
|
||||
function collectSnapshotDependencies(lines, startIndex) {
|
||||
const result = collectIndentedStringMap(lines, startIndex, MAPPING_ENTRY_INDENT);
|
||||
return { dependencies: result.entries, nextIndex: result.nextIndex };
|
||||
}
|
||||
|
||||
function parsePnpmLockfileSections(lockfileText) {
|
||||
// Keep this parser dependency-free: security-fast runs this hook without pnpm install.
|
||||
// It only needs the small pnpm-lock subset used to collect production snapshots.
|
||||
const importers = [];
|
||||
const snapshots = {};
|
||||
const lines = lockfileText.split(/\r?\n/u);
|
||||
let currentTopLevelSection = null;
|
||||
let hasImportersSection = false;
|
||||
let hasSnapshotsSection = false;
|
||||
|
||||
for (let index = 0; index < lines.length; ) {
|
||||
const line = lines[index];
|
||||
const trimmed = line.trim();
|
||||
const indentation = countIndentation(line);
|
||||
|
||||
if (isIgnorableYamlLine(trimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (indentation === TOP_LEVEL_INDENT && trimmed.endsWith(":")) {
|
||||
currentTopLevelSection = parseYamlScalar(trimmed.slice(0, -1));
|
||||
if (currentTopLevelSection === "importers") {
|
||||
hasImportersSection = true;
|
||||
}
|
||||
if (currentTopLevelSection === "snapshots") {
|
||||
hasSnapshotsSection = true;
|
||||
}
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (
|
||||
currentTopLevelSection === "importers" &&
|
||||
indentation === SECTION_ENTRY_INDENT &&
|
||||
trimmed.endsWith(":")
|
||||
) {
|
||||
index += 1;
|
||||
while (index < lines.length) {
|
||||
const nestedLine = lines[index];
|
||||
const nestedTrimmed = nestedLine.trim();
|
||||
const nestedIndentation = countIndentation(nestedLine);
|
||||
|
||||
if (isIgnorableYamlLine(nestedTrimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (nestedIndentation <= SECTION_ENTRY_INDENT) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
nestedIndentation === NESTED_SECTION_INDENT &&
|
||||
isNamedYamlSection(nestedTrimmed, IMPORTER_SECTIONS)
|
||||
) {
|
||||
const result = collectImporterDependencyReferences(lines, index + 1);
|
||||
importers.push(...result.references);
|
||||
index = result.nextIndex;
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if (currentTopLevelSection === "snapshots" && indentation === SECTION_ENTRY_INDENT) {
|
||||
const snapshotEntry = parseYamlMappingLine(trimmed);
|
||||
if (!snapshotEntry) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (snapshotEntry.value) {
|
||||
snapshots[snapshotEntry.key] = {};
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const snapshotKey = snapshotEntry.key;
|
||||
const snapshot = {};
|
||||
index += 1;
|
||||
while (index < lines.length) {
|
||||
const nestedLine = lines[index];
|
||||
const nestedTrimmed = nestedLine.trim();
|
||||
const nestedIndentation = countIndentation(nestedLine);
|
||||
|
||||
if (isIgnorableYamlLine(nestedTrimmed)) {
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (nestedIndentation <= SECTION_ENTRY_INDENT) {
|
||||
break;
|
||||
}
|
||||
if (
|
||||
nestedIndentation === NESTED_SECTION_INDENT &&
|
||||
isNamedYamlSection(nestedTrimmed, SNAPSHOT_SECTIONS)
|
||||
) {
|
||||
const result = collectSnapshotDependencies(lines, index + 1);
|
||||
snapshot[nestedTrimmed.slice(0, -1)] = result.dependencies;
|
||||
index = result.nextIndex;
|
||||
continue;
|
||||
}
|
||||
index += 1;
|
||||
}
|
||||
snapshots[snapshotKey] = snapshot;
|
||||
continue;
|
||||
}
|
||||
|
||||
index += 1;
|
||||
}
|
||||
|
||||
return { hasImportersSection, hasSnapshotsSection, importers, snapshots };
|
||||
}
|
||||
|
||||
function resolveSnapshot({ dependencyName, reference, snapshots }) {
|
||||
if (isLocalReference(reference)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const directKey = `${dependencyName}@${reference}`;
|
||||
if (directKey in snapshots) {
|
||||
return {
|
||||
snapshotKey: directKey,
|
||||
...parseSnapshotKey(directKey),
|
||||
};
|
||||
}
|
||||
|
||||
if (reference in snapshots) {
|
||||
return {
|
||||
snapshotKey: reference,
|
||||
...parseSnapshotKey(reference),
|
||||
};
|
||||
}
|
||||
|
||||
if (reference.startsWith("npm:")) {
|
||||
const aliasKey = reference.slice(4);
|
||||
if (aliasKey in snapshots) {
|
||||
return {
|
||||
snapshotKey: aliasKey,
|
||||
...parseSnapshotKey(aliasKey),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Unable to resolve pnpm snapshot for dependency "${dependencyName}" with reference "${reference}".`,
|
||||
);
|
||||
}
|
||||
|
||||
export function collectProdResolvedPackagesFromLockfile(lockfileText) {
|
||||
const lockfile = parsePnpmLockfileSections(lockfileText);
|
||||
if (!lockfile.hasImportersSection) {
|
||||
throw new Error("pnpm-lock.yaml is missing the importers section.");
|
||||
}
|
||||
if (!lockfile.hasSnapshotsSection) {
|
||||
throw new Error("pnpm-lock.yaml is missing the snapshots section.");
|
||||
}
|
||||
|
||||
const versionsByPackage = new Map();
|
||||
const seenSnapshots = new Set();
|
||||
const queue = [...lockfile.importers];
|
||||
|
||||
while (queue.length > 0) {
|
||||
const next = queue.pop();
|
||||
if (!next) {
|
||||
continue;
|
||||
}
|
||||
const resolved = resolveSnapshot({
|
||||
dependencyName: next.dependencyName,
|
||||
reference: next.reference,
|
||||
snapshots: lockfile.snapshots,
|
||||
});
|
||||
if (!resolved) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let versions = versionsByPackage.get(resolved.packageName);
|
||||
if (!versions) {
|
||||
versions = new Set();
|
||||
versionsByPackage.set(resolved.packageName, versions);
|
||||
}
|
||||
versions.add(resolved.version);
|
||||
|
||||
if (seenSnapshots.has(resolved.snapshotKey)) {
|
||||
continue;
|
||||
}
|
||||
seenSnapshots.add(resolved.snapshotKey);
|
||||
|
||||
const snapshot = lockfile.snapshots[resolved.snapshotKey];
|
||||
if (!snapshot || typeof snapshot !== "object") {
|
||||
continue;
|
||||
}
|
||||
for (const sectionName of SNAPSHOT_SECTIONS) {
|
||||
const dependencies = snapshot[sectionName];
|
||||
if (!dependencies || typeof dependencies !== "object") {
|
||||
continue;
|
||||
}
|
||||
for (const [dependencyName, reference] of Object.entries(dependencies)) {
|
||||
if (typeof reference !== "string") {
|
||||
continue;
|
||||
}
|
||||
queue.push({ dependencyName, reference });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return versionsByPackage;
|
||||
}
|
||||
|
||||
export function collectAllResolvedPackagesFromLockfile(lockfileText) {
|
||||
const lockfile = parsePnpmLockfileSections(lockfileText);
|
||||
if (!lockfile.hasSnapshotsSection) {
|
||||
throw new Error("pnpm-lock.yaml is missing the snapshots section.");
|
||||
}
|
||||
|
||||
const versionsByPackage = new Map();
|
||||
for (const snapshotKey of Object.keys(lockfile.snapshots)) {
|
||||
const resolved = parseSnapshotKey(snapshotKey);
|
||||
let versions = versionsByPackage.get(resolved.packageName);
|
||||
if (!versions) {
|
||||
versions = new Set();
|
||||
versionsByPackage.set(resolved.packageName, versions);
|
||||
}
|
||||
versions.add(resolved.version);
|
||||
}
|
||||
|
||||
return versionsByPackage;
|
||||
}
|
||||
|
||||
export function createBulkAdvisoryPayload(versionsByPackage) {
|
||||
return Object.fromEntries(
|
||||
[...versionsByPackage.entries()]
|
||||
.toSorted(([left], [right]) => left.localeCompare(right))
|
||||
.map(([packageName, versions]) => [
|
||||
packageName,
|
||||
[...versions].toSorted((left, right) => left.localeCompare(right)),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeSeverity(severity) {
|
||||
if (typeof severity !== "string") {
|
||||
return "info";
|
||||
}
|
||||
return severity.toLowerCase();
|
||||
}
|
||||
|
||||
function advisoryMatchesOverride(advisory, override) {
|
||||
const advisoryId = String(advisory?.id ?? "");
|
||||
const advisoryUrl = typeof advisory?.url === "string" ? advisory.url : "";
|
||||
return (
|
||||
override.advisoryIds.has(advisoryId) ||
|
||||
[...override.advisoryIds].some((id) => advisoryUrl.includes(id))
|
||||
);
|
||||
}
|
||||
|
||||
function shouldSuppressAdvisoryFinding({ packageName, advisory, versionsByPackage }) {
|
||||
if (!versionsByPackage) {
|
||||
return false;
|
||||
}
|
||||
const override = AUDIT_ADVISORY_VERSION_OVERRIDES.find(
|
||||
(candidate) =>
|
||||
candidate.packageName === packageName && advisoryMatchesOverride(advisory, candidate),
|
||||
);
|
||||
if (!override) {
|
||||
return false;
|
||||
}
|
||||
const resolvedVersions = versionsByPackage.get(packageName);
|
||||
if (!resolvedVersions || resolvedVersions.size === 0) {
|
||||
return false;
|
||||
}
|
||||
return [...resolvedVersions].every((version) => override.unaffectedVersions.has(version));
|
||||
}
|
||||
|
||||
export function filterFindingsBySeverity(advisoriesByPackage, minSeverity, versionsByPackage) {
|
||||
const threshold = normalizeAuditLevel(minSeverity);
|
||||
const findings = [];
|
||||
|
||||
for (const [packageName, advisories] of Object.entries(advisoriesByPackage ?? {})) {
|
||||
if (!Array.isArray(advisories)) {
|
||||
continue;
|
||||
}
|
||||
for (const advisory of advisories) {
|
||||
if (!advisory || typeof advisory !== "object") {
|
||||
continue;
|
||||
}
|
||||
const severity = normalizeSeverity(advisory.severity);
|
||||
if ((SEVERITY_RANK[severity] ?? -1) < SEVERITY_RANK[threshold]) {
|
||||
continue;
|
||||
}
|
||||
if (shouldSuppressAdvisoryFinding({ packageName, advisory, versionsByPackage })) {
|
||||
continue;
|
||||
}
|
||||
findings.push({
|
||||
packageName,
|
||||
id: advisory.id ?? "unknown",
|
||||
severity,
|
||||
title: advisory.title ?? "Untitled advisory",
|
||||
url: advisory.url ?? null,
|
||||
vulnerableVersions: advisory.vulnerable_versions ?? null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
findings.sort((left, right) => {
|
||||
const severityDelta =
|
||||
(SEVERITY_RANK[right.severity] ?? -1) - (SEVERITY_RANK[left.severity] ?? -1);
|
||||
if (severityDelta !== 0) {
|
||||
return severityDelta;
|
||||
}
|
||||
return left.packageName.localeCompare(right.packageName);
|
||||
});
|
||||
|
||||
return findings;
|
||||
}
|
||||
|
||||
function chunkEntries(entries, size) {
|
||||
const chunks = [];
|
||||
for (let index = 0; index < entries.length; index += size) {
|
||||
chunks.push(entries.slice(index, index + size));
|
||||
}
|
||||
return chunks;
|
||||
}
|
||||
|
||||
function resolveRegistryBaseUrl() {
|
||||
const configured =
|
||||
process.env.npm_config_registry ??
|
||||
process.env.NPM_CONFIG_REGISTRY ??
|
||||
process.env.npm_config_userconfig_registry ??
|
||||
DEFAULT_REGISTRY;
|
||||
return configured.replace(/\/+$/u, "");
|
||||
}
|
||||
|
||||
function parsePositiveIntegerEnv(name, fallback) {
|
||||
const raw = process.env[name]?.trim();
|
||||
if (!raw) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number.parseInt(raw, 10);
|
||||
if (!Number.isSafeInteger(parsed) || parsed < 1 || String(parsed) !== raw) {
|
||||
throw new Error(`${name} must be a positive integer`);
|
||||
}
|
||||
return parsed;
|
||||
}
|
||||
|
||||
function resolveBulkAdvisoryRequestTimeoutMs() {
|
||||
return clampTimerTimeoutMs(
|
||||
parsePositiveIntegerEnv(
|
||||
"OPENCLAW_PNPM_AUDIT_BULK_TIMEOUT_MS",
|
||||
BULK_ADVISORY_REQUEST_TIMEOUT_MS,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveBulkAdvisoryResponseBodyMaxBytes() {
|
||||
return parsePositiveIntegerEnv(
|
||||
"OPENCLAW_PNPM_AUDIT_BULK_RESPONSE_MAX_BYTES",
|
||||
BULK_ADVISORY_RESPONSE_BODY_MAX_BYTES,
|
||||
);
|
||||
}
|
||||
|
||||
function clampTimerTimeoutMs(valueMs) {
|
||||
const value = Number.isFinite(valueMs) ? valueMs : BULK_ADVISORY_REQUEST_TIMEOUT_MS;
|
||||
return Math.min(Math.max(Math.floor(value), 1), MAX_TIMER_TIMEOUT_MS);
|
||||
}
|
||||
|
||||
async function withBulkAdvisoryTimeout({ label, timeoutMs, run }) {
|
||||
const resolvedTimeoutMs = clampTimerTimeoutMs(timeoutMs);
|
||||
const controller = new AbortController();
|
||||
let timeout;
|
||||
const timeoutPromise = new Promise((_resolve, reject) => {
|
||||
timeout = setTimeout(() => {
|
||||
const error = new Error(`${label} exceeded timeout of ${resolvedTimeoutMs}ms`);
|
||||
controller.abort(error);
|
||||
reject(error);
|
||||
}, resolvedTimeoutMs);
|
||||
});
|
||||
try {
|
||||
return await Promise.race([run({ signal: controller.signal, timeoutPromise }), timeoutPromise]);
|
||||
} finally {
|
||||
if (timeout) {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function readBoundedResponseText(response, maxBytes, label, options = {}) {
|
||||
return await readBoundedResponseTextWithLimit(response, label, maxBytes, {
|
||||
signal: options.signal,
|
||||
timeoutPromise: options.timeoutPromise,
|
||||
formatTooLargeMessage: (messageLabel, bytes) => `${messageLabel} exceeded ${bytes} bytes`,
|
||||
createTooLargeError: (message) => Object.assign(new Error(message), { code: "ETOOBIG" }),
|
||||
});
|
||||
}
|
||||
|
||||
export async function readBoundedBulkAdvisoryErrorText(
|
||||
response,
|
||||
maxChars = BULK_ADVISORY_ERROR_BODY_MAX_CHARS,
|
||||
options = {},
|
||||
) {
|
||||
if (!response.body) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let text = "";
|
||||
let truncated = false;
|
||||
let canceled = false;
|
||||
|
||||
try {
|
||||
while (text.length <= maxChars) {
|
||||
const read = reader.read();
|
||||
const readWithTimeout = options.timeoutPromise
|
||||
? Promise.race([
|
||||
read,
|
||||
options.timeoutPromise.catch((error) => {
|
||||
canceled = true;
|
||||
void Promise.resolve()
|
||||
.then(() => reader.cancel())
|
||||
.catch(() => undefined);
|
||||
throw error;
|
||||
}),
|
||||
])
|
||||
: read;
|
||||
const { done, value } = await readWithTimeout;
|
||||
if (done) {
|
||||
text += decoder.decode();
|
||||
break;
|
||||
}
|
||||
|
||||
text += decoder.decode(value, { stream: true });
|
||||
if (text.length > maxChars) {
|
||||
text = text.slice(0, maxChars);
|
||||
truncated = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (truncated) {
|
||||
await reader.cancel().catch(() => undefined);
|
||||
} else if (!canceled) {
|
||||
reader.releaseLock();
|
||||
}
|
||||
}
|
||||
|
||||
return truncated ? `${text}\n[truncated]` : text;
|
||||
}
|
||||
|
||||
async function readBulkAdvisoryJson(response, maxBytes, options = {}) {
|
||||
const text = await readBoundedResponseText(
|
||||
response,
|
||||
maxBytes,
|
||||
"Bulk advisory response body",
|
||||
options,
|
||||
);
|
||||
if (!text.trim()) {
|
||||
throw new Error("Bulk advisory response body was empty");
|
||||
}
|
||||
return JSON.parse(text);
|
||||
}
|
||||
|
||||
export async function fetchBulkAdvisories({
|
||||
payload,
|
||||
fetchImpl = fetch,
|
||||
registryBaseUrl = resolveRegistryBaseUrl(),
|
||||
responseBodyMaxBytes = resolveBulkAdvisoryResponseBodyMaxBytes(),
|
||||
timeoutMs = resolveBulkAdvisoryRequestTimeoutMs(),
|
||||
}) {
|
||||
const url = `${registryBaseUrl}${BULK_ADVISORY_PATH}`;
|
||||
return await withBulkAdvisoryTimeout({
|
||||
label: "Bulk advisory request",
|
||||
timeoutMs,
|
||||
run: async ({ signal, timeoutPromise }) => {
|
||||
const response = await fetchImpl(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
"content-type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const bodyText = await readBoundedBulkAdvisoryErrorText(response, undefined, {
|
||||
timeoutPromise,
|
||||
});
|
||||
throw new Error(
|
||||
`Bulk advisory request failed (${response.status} ${response.statusText}): ${bodyText}`,
|
||||
);
|
||||
}
|
||||
|
||||
return await readBulkAdvisoryJson(response, responseBodyMaxBytes, {
|
||||
signal,
|
||||
timeoutPromise,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export async function runPnpmAuditProd({
|
||||
rootDir = process.cwd(),
|
||||
fetchImpl = fetch,
|
||||
stdout = process.stdout,
|
||||
stderr = process.stderr,
|
||||
minSeverity = MIN_SEVERITY,
|
||||
} = {}) {
|
||||
const normalizedMinSeverity = normalizeAuditLevel(minSeverity);
|
||||
const lockfilePath = path.join(rootDir, "pnpm-lock.yaml");
|
||||
const lockfileText = await readFile(lockfilePath, "utf8");
|
||||
const versionsByPackage = collectProdResolvedPackagesFromLockfile(lockfileText);
|
||||
const payload = createBulkAdvisoryPayload(versionsByPackage);
|
||||
const payloadEntries = Object.entries(payload);
|
||||
|
||||
if (payloadEntries.length === 0) {
|
||||
stdout.write("No production dependencies found in pnpm-lock.yaml.\n");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const advisoryResults = {};
|
||||
for (const payloadChunk of chunkEntries(payloadEntries, 400)) {
|
||||
const chunkPayload = Object.fromEntries(payloadChunk);
|
||||
const chunkResults = await fetchBulkAdvisories({
|
||||
payload: chunkPayload,
|
||||
fetchImpl,
|
||||
});
|
||||
Object.assign(advisoryResults, chunkResults);
|
||||
}
|
||||
|
||||
const findings = filterFindingsBySeverity(
|
||||
advisoryResults,
|
||||
normalizedMinSeverity,
|
||||
versionsByPackage,
|
||||
);
|
||||
if (findings.length === 0) {
|
||||
stdout.write(
|
||||
`No ${normalizedMinSeverity} or higher advisories found for production dependencies.\n`,
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
|
||||
stderr.write(
|
||||
`Found ${findings.length} ${normalizedMinSeverity} or higher advisories in production dependencies:\n`,
|
||||
);
|
||||
for (const finding of findings.slice(0, 25)) {
|
||||
const details = [
|
||||
`${finding.severity.toUpperCase()} ${finding.packageName}`,
|
||||
`id=${finding.id}`,
|
||||
`title=${finding.title}`,
|
||||
];
|
||||
if (finding.vulnerableVersions) {
|
||||
details.push(`range=${finding.vulnerableVersions}`);
|
||||
}
|
||||
if (finding.url) {
|
||||
details.push(`url=${finding.url}`);
|
||||
}
|
||||
stderr.write(`- ${details.join(" · ")}\n`);
|
||||
}
|
||||
if (findings.length > 25) {
|
||||
stderr.write(`...and ${findings.length - 25} more advisories.\n`);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
function readSeverityValue(value, optionName) {
|
||||
if (value === undefined || value === "" || value.startsWith("-")) {
|
||||
throw new Error(`${optionName} requires a value`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
export function parseArgs(argv) {
|
||||
let minSeverity = MIN_SEVERITY;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const argument = argv[index];
|
||||
if (argument === "--audit-level" || argument === "--min-severity") {
|
||||
minSeverity = readSeverityValue(argv[index + 1], argument);
|
||||
index += 1;
|
||||
continue;
|
||||
}
|
||||
if (argument.startsWith("--audit-level=")) {
|
||||
minSeverity = readSeverityValue(argument.slice("--audit-level=".length), "--audit-level");
|
||||
continue;
|
||||
}
|
||||
if (argument.startsWith("--min-severity=")) {
|
||||
minSeverity = readSeverityValue(argument.slice("--min-severity=".length), "--min-severity");
|
||||
continue;
|
||||
}
|
||||
throw new Error(`Unknown argument "${argument}".`);
|
||||
}
|
||||
|
||||
return { minSeverity };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
try {
|
||||
const { minSeverity } = parseArgs(process.argv.slice(2));
|
||||
process.exitCode = await runPnpmAuditProd({ minSeverity });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
process.stderr.write(`${message}\n`);
|
||||
process.exitCode = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
|
||||
await main();
|
||||
}
|
||||
Reference in New Issue
Block a user