diff --git a/.github/workflows/security-audit.yml b/.github/workflows/security-audit.yml index cb93c81..d25b5e6 100644 --- a/.github/workflows/security-audit.yml +++ b/.github/workflows/security-audit.yml @@ -22,4 +22,52 @@ jobs: - uses: actions/setup-node@v7 with: node-version: 22 - - run: pnpm audit --audit-level low + - name: Install dependencies for complete audit paths + run: pnpm install --frozen-lockfile --ignore-scripts + - name: Audit dependencies (two approved exceptions expire October 22) + env: + AUDIT_EXCEPTIONS_EXPIRE_AT: '2026-10-22T05:37:00Z' + run: | + # Omer approved these two temporary risks on 2026-10-08, not a release. + # braces: CLI glob size/depth guards mitigate input; tooling remains affected. + # http-cache-semantics: reviewed Lerna callers use private caches on fresh runners. + # Remove exceptions when patched; expiry never rolls forward automatically. + report="$RUNNER_TEMP/dependency-audit.json" + audit_status=0 + pnpm audit --json > "$report" || audit_status=$? + cat "$report" + # Registry errors, muted findings, unexpected statuses/schema must fail closed. + jq -e --argjson status "$audit_status" ' + type == "object" and (has("error") | not) and .muted == [] and + (.advisories | type == "object") and + (.metadata.vulnerabilities | keys == ["critical", "high", "info", "low", "moderate"]) and + (.metadata.vulnerabilities | all(.[]; type == "number" and . >= 0)) and + (([.metadata.vulnerabilities[]] | add) == (.advisories | length)) and + (($status == 0 and (.advisories | length) == 0) or + ($status == 1 and (.advisories | length) > 0)) and + (.advisories | all(.[]; + (.module_name | type == "string") and + (.findings | type == "array" and length > 0) and + (.findings | all(.[]; (.version | type == "string") and + (.paths | type == "array" and length > 0) and + (.paths | all(.[]; type == "string")))))) + ' "$report" > /dev/null + jq --arg expires "$AUDIT_EXCEPTIONS_EXPIRE_AT" --argjson now "$(date +%s)" ' + [.advisories[] | select( + ($now < ($expires | fromdateiso8601) and + ((.github_advisory_id == "GHSA-vfj7-8cjw-p6xm" and + .module_name == "braces" and all(.findings[]; .version == "3.0.3")) or + (.github_advisory_id == "GHSA-ch52-4w7c-c8xp" and + .module_name == "http-cache-semantics" and + all(.findings[]; .version == "4.2.0" and + all(.paths[]; startswith(". > lerna@9.0.7 > ")))))) | not)] + ' "$report" > "$RUNNER_TEMP/blocking-advisories.json" + if ! jq -e 'length == 0' "$RUNNER_TEMP/blocking-advisories.json" > /dev/null; then + cat "$RUNNER_TEMP/blocking-advisories.json" + echo "::error::Audit contains unapproved findings or the temporary exceptions have expired." + exit 1 + fi + if jq -e '.advisories | length > 0' "$report" > /dev/null; then + echo "::warning::Audit passed with approved risk exceptions until $AUDIT_EXCEPTIONS_EXPIRE_AT; dependencies remain affected." + echo "Audit: temporary risk accepted for the reported braces/http-cache-semantics findings until $AUDIT_EXCEPTIONS_EXPIRE_AT. Not release approval." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/package.json b/package.json index 8d4bb5f..e854dd8 100644 --- a/package.json +++ b/package.json @@ -68,11 +68,14 @@ "vite@>=7.0.0 <7.3.5": "^7.3.5", "esbuild@>=0.27.3 <0.28.1": "0.28.1", "tar@<7.5.21": "7.5.21", - "brace-expansion@>=5.0.0 <5.0.9": "5.0.9", + "brace-expansion@>=5.0.0 <5.0.12": "5.0.12", "js-yaml@>=4.0.0 <4.3.2": "4.3.2", - "axios@>=1.0.0 <1.18.0": "1.18.0", + "axios@>=1.0.0 <1.20.0": "1.20.0", "pacote@>=21.0.0 <21.5.1": "21.5.1", - "smol-toml@<1.7.1": "1.7.1" + "smol-toml@<1.9.0": "1.9.0", + "nx@>=22.0.0 <22.7.10": "22.7.10", + "source-map-js@>=1.0.0 <1.2.2": "1.2.2", + "postcss-selector-parser@<7.1.6": "7.1.6" } } } diff --git a/packages/cli/src/config/loader.ts b/packages/cli/src/config/loader.ts index 7a8d65d..ba8633b 100644 --- a/packages/cli/src/config/loader.ts +++ b/packages/cli/src/config/loader.ts @@ -10,6 +10,10 @@ import { validateConfig, formatValidationErrors } from './validator.js'; */ const CONFIG_FILENAMES = ['contractual.yaml', 'contractual.yml']; +// Bound the input and recursive AST depth before fast-glob invokes braces. +const MAX_GLOB_PATTERN_LENGTH = 1024; +const MAX_GLOB_NESTING = 32; + /** * Error thrown when config cannot be loaded */ @@ -20,6 +24,58 @@ export class ConfigError extends Error { } } +function validateGlobPattern(pattern: string, contractName: string): void { + if (pattern.length > MAX_GLOB_PATTERN_LENGTH) { + throw new ConfigError( + `Glob for contract "${contractName}" exceeds ${MAX_GLOB_PATTERN_LENGTH} characters after resolving its path` + ); + } + + const nesting: string[] = []; + let quote: string | undefined; + let brackets = 0; + + for (let index = 0; index < pattern.length; index++) { + const character = pattern[index]; + if (character === '\\') { + index++; + continue; + } + if (quote) { + if (character === quote) quote = undefined; + continue; + } + if (brackets > 0) { + if (character === '[') brackets++; + if (character === ']') brackets--; + continue; + } + if (character === '[') { + brackets++; + continue; + } + if (character === '"' || character === "'" || character === '`') { + quote = character; + continue; + } + // Parentheses also create recursive nodes in the braces parser. Only a + // matching closer ends a node; quoted/class/escaped closers are literals. + if (character === '{' || character === '(') { + nesting.push(character); + if (nesting.length > MAX_GLOB_NESTING) { + throw new ConfigError( + `Glob for contract "${contractName}" exceeds ${MAX_GLOB_NESTING} levels of brace/parenthesis nesting` + ); + } + } else if ( + (character === '}' && nesting.at(-1) === '{') || + (character === ')' && nesting.at(-1) === '(') + ) { + nesting.pop(); + } + } +} + /** * Find contractual.yaml by walking up from the given directory */ @@ -79,6 +135,7 @@ export function resolveContractPaths( // Check if it's a glob pattern if (pattern.includes('*')) { + validateGlobPattern(absolutePattern, contract.name); const matches = fg.sync(absolutePattern, { onlyFiles: true }); if (matches.length === 0) { console.warn( diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a188fd5..abe82b2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -11,11 +11,14 @@ overrides: vite@>=7.0.0 <7.3.5: ^7.3.5 esbuild@>=0.27.3 <0.28.1: 0.28.1 tar@<7.5.21: 7.5.21 - brace-expansion@>=5.0.0 <5.0.9: 5.0.9 + brace-expansion@>=5.0.0 <5.0.12: 5.0.12 js-yaml@>=4.0.0 <4.3.2: 4.3.2 - axios@>=1.0.0 <1.18.0: 1.18.0 + axios@>=1.0.0 <1.20.0: 1.20.0 pacote@>=21.0.0 <21.5.1: 21.5.1 - smol-toml@<1.7.1: 1.7.1 + smol-toml@<1.9.0: 1.9.0 + nx@>=22.0.0 <22.7.10: 22.7.10 + source-map-js@>=1.0.0 <1.2.2: 1.2.2 + postcss-selector-parser@<7.1.6: 7.1.6 importers: @@ -862,55 +865,55 @@ packages: '@nx/devkit@22.7.7': resolution: {integrity: sha512-XU9LZuqT4+/2DTrpKnGDuGFKjwe7LRr5kMfT6EOPiPYCYkmtwXriMGiaRZNabb6kliVG3L34L7+TEiMUWRPCDQ==} peerDependencies: - nx: '>= 21 <= 23 || ^22.0.0-0' + nx: 22.7.10 - '@nx/nx-darwin-arm64@22.7.7': - resolution: {integrity: sha512-HGu8Bc3DzmIFeKHzIEDBDScgI8/hOjVIj/EzJBE5289dUf+CQ5OSXc6kw2NL9SiCMAPKa0clqcJgLJLqP5t/DQ==} + '@nx/nx-darwin-arm64@22.7.10': + resolution: {integrity: sha512-gy0ASfsnFsMmTLQaGTQyf8l95kAgawBpdMrgtxsnXzyKAasqzfIEhEbb74/6+DmsmcnNyfBvAKvVvFBiNs3ZYw==} cpu: [arm64] os: [darwin] - '@nx/nx-darwin-x64@22.7.7': - resolution: {integrity: sha512-J7z/Hh+bEKB9xvHdmtO5sK7YytFx8Ry6YpvYI2PoSl2/xDBca7q69fzHKTtm4S/8FvA1Qoi/m//JTIMQUB7cKA==} + '@nx/nx-darwin-x64@22.7.10': + resolution: {integrity: sha512-IGNSVU85ktrNm9cH9yLgSaQjmRVCcEldaZwtAqNaMcNU+Go5xY4zYv8tDQ2MJ3hXpi6pNdcFzZp8tZIJliHyPA==} cpu: [x64] os: [darwin] - '@nx/nx-freebsd-x64@22.7.7': - resolution: {integrity: sha512-9H4bbI3fBr8Ct+6xmF6dqDKEvER60XV3tanXTSVtUBxyAVw3cPmRh7aMAu5PNyN+ewzRAJvV6KI4Rx1kA+s9jw==} + '@nx/nx-freebsd-x64@22.7.10': + resolution: {integrity: sha512-KlmBmvD8uE7Saah3DO61D7ywn08FtYJE0I0AwZGs+0cisY1bUawtZZNYvbPTprHaCOWvbU2GeIVstK34vmbVXg==} cpu: [x64] os: [freebsd] - '@nx/nx-linux-arm-gnueabihf@22.7.7': - resolution: {integrity: sha512-QsGaaZ7PI18c7rFgvUe0F0UYa5oErSKY6Yff6HVXy2zAAWluM4F/TXIRYbtXlvSq++CHhFXxk29Xb3+/zGR/rg==} + '@nx/nx-linux-arm-gnueabihf@22.7.10': + resolution: {integrity: sha512-xwCv9gR968hMeeWUqBneLR9nBqP+ECPVFumdWCxMBh116ftCjMMik8E3BdUKLdqreVYFJdwvJRPortHj/YYikA==} cpu: [arm] os: [linux] - '@nx/nx-linux-arm64-gnu@22.7.7': - resolution: {integrity: sha512-CfzWaS+b32lI/inlYPv1ZAK9CWTWFHzT7TPThvOHML65nrgFl8cQ4Z4FeGdyCbHu2NoG3vR1E36O2tPI2/DLGg==} + '@nx/nx-linux-arm64-gnu@22.7.10': + resolution: {integrity: sha512-AiwBgerLicRzcyEevqwDuP22Nj5nFFrjJsCeZ+D4rC2HmS4LdBzXb+BBuHAZTysCo9QClk1t9VuVwwKyorU7ZA==} cpu: [arm64] os: [linux] - '@nx/nx-linux-arm64-musl@22.7.7': - resolution: {integrity: sha512-53QFuODMEHc1gobCT2WOmYz89DZtEIuLphirdIgnXkkPBTdrP77LPbJUXMRXCMKr90BQaSWhTX+J/ubANRE8og==} + '@nx/nx-linux-arm64-musl@22.7.10': + resolution: {integrity: sha512-8GNi8v6Ej/TyAaVBiFRAwFYrqdje/dyFbPzpq895m3ow8zbPrYmoz/qizrgkRzBFulgkSxu33LXi388reVLi2w==} cpu: [arm64] os: [linux] - '@nx/nx-linux-x64-gnu@22.7.7': - resolution: {integrity: sha512-unzmqGIDXGzujo1ZtbrMj2e5D5ldQ1feZmqDPhvO4casK2d96jpT8LtgIILpVDUfhLjl+By185gb0ArrWTsyKw==} + '@nx/nx-linux-x64-gnu@22.7.10': + resolution: {integrity: sha512-spcDvoQK/mrSvnHFdophlwgzUoa8lYXAZTk15+w/gLpaowP5hR4mInhUWsU9VuEoe41J3x+FcFwmw01Djj6KNQ==} cpu: [x64] os: [linux] - '@nx/nx-linux-x64-musl@22.7.7': - resolution: {integrity: sha512-oTjMGuM105ok8aH5rlg2YP0Kgkjg0VfUj1exwp49S70gGBJ0r8v5rEtJwOSdCf1WTHuEh0xi66Y/b1S0khF8dg==} + '@nx/nx-linux-x64-musl@22.7.10': + resolution: {integrity: sha512-76PdpGIQ1PVA77sYM4Ons779Yx7y3H+Hlsax2HhifAv8dKhAz9/Lmhst9ZqS2zU5v03lGEnN77W4KZQHJBiltA==} cpu: [x64] os: [linux] - '@nx/nx-win32-arm64-msvc@22.7.7': - resolution: {integrity: sha512-K741meG/l48TPPeDqnhYTV7pP+1ljjZ+0DRJBxMrPFIu8qKE3Abko/7NKWmWRjcSXJvtxvYkgG3wZds4N2Bhow==} + '@nx/nx-win32-arm64-msvc@22.7.10': + resolution: {integrity: sha512-dARAojqVyvE88Mt1ms6+UTb3TIAoo4xzkPaX05Qcmh/c8+SQgA24JuqiRp+4kRz1voYwFexRpWpcrfpKJ0Nr+Q==} cpu: [arm64] os: [win32] - '@nx/nx-win32-x64-msvc@22.7.7': - resolution: {integrity: sha512-AHVjXkreXjVKAKpMNCsF5nQC/et6lpQcERRm8AYOCncI59hTjYOLpdCMBLrIHA3hgc6SUYU8n5+tMAbv7zXXiQ==} + '@nx/nx-win32-x64-msvc@22.7.10': + resolution: {integrity: sha512-qlH70TBMrRw/5pMg6MtPXkuM3LzDHszpqWcJ4NPdipLsyD6T+B+NteanlPtxZrdfqi+8Hd/7fv2OkllA8iY1fg==} cpu: [x64] os: [win32] @@ -1536,8 +1539,8 @@ packages: resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} engines: {node: '>= 0.4'} - axios@1.18.0: - resolution: {integrity: sha512-E32NzpYKp++W7XRe52rHiXV2ehxmh3wbdgO7MHeFM+vqxLBYHzt0ElkiImtOBxtOmyp0yoC8C6uESVV84Y2/hw==} + axios@1.20.0: + resolution: {integrity: sha512-r8aOh8j9cGKpgQAqpzrUHnSIc6a59Y3Xf/cv8sy1DrHCkZHzQGEuoq1tARk6qSyDdtQGSDgpb9kFlruzPvrgwg==} balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} @@ -1569,8 +1572,8 @@ packages: brace-expansion@2.1.7: resolution: {integrity: sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==} - brace-expansion@5.0.9: - resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + brace-expansion@5.0.12: + resolution: {integrity: sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==} engines: {node: 20 || >=22} braces@3.0.3: @@ -3164,8 +3167,8 @@ packages: resolution: {integrity: sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - nx@22.7.7: - resolution: {integrity: sha512-T4hq5PMAPXeqAmm0Y6Gr5ApeI2I+T8MflWEKN1cZ12R1Rnq2uEpfOgiwAZ74bHONmNgv6LRG9P9+x8SX2tDIlw==} + nx@22.7.10: + resolution: {integrity: sha512-T22WdfbkRi9cLH8a+FkKsNrt540DpNkzkFv1StSsphfu1povyqoVQw9Rpl/poNvnAICNKPwZcpGsy/QXMYkECw==} hasBin: true peerDependencies: '@swc-node/register': ^1.11.1 @@ -3415,8 +3418,8 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} - postcss-selector-parser@7.1.4: - resolution: {integrity: sha512-HeP7D2wyhkR+XaK6v4W8oRF62Dsz4flyuczALJp61GckGm42u1saSSJ/0auvcBqxs3jMRFEcPK34At/0JBKdOg==} + postcss-selector-parser@7.1.6: + resolution: {integrity: sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw==} engines: {node: '>=4'} postcss@8.5.24: @@ -3490,8 +3493,8 @@ packages: react-is@18.3.1: resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==} - react-is@19.3.0: - resolution: {integrity: sha512-UpMYezM4v5/18F28aC66AEsjXIgE02kyEMH6yLdgLXu/UTfa1Ntwck/nNLrbqJsEXW7gPb0coNO9FQse9WTovA==} + react-is@19.2.8: + resolution: {integrity: sha512-s5un28nYxKJw5gvUHyW5PCC28CvBqLu9r3cWgzHT4Vo/5fqqkFcdRYsGcKf50WMPpjjFZS5d76fn3YCo2njKwQ==} read-cmd-shim@4.0.0: resolution: {integrity: sha512-yILWifhaSEEytfXI76kB9xEEiG1AiozaCJZ83A87ytjRiN+jVibXjedjCRNjoZviinhG+4UkalO3mWTd8u5O0Q==} @@ -3731,8 +3734,8 @@ packages: resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - smol-toml@1.7.1: - resolution: {integrity: sha512-PPlsspAZ4jbMBu5DMFhfUGDQLu/vrL4SyBROVS37x8ynnVmFIs1VPBz1Co8Xks3TvpIaZXmU85y4DrQ+UyVFoQ==} + smol-toml@1.9.0: + resolution: {integrity: sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==} engines: {node: '>= 18'} socks-proxy-agent@8.0.5: @@ -3743,8 +3746,8 @@ packages: resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - source-map-js@1.2.1: - resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + source-map-js@1.2.2: + resolution: {integrity: sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==} engines: {node: '>=0.10.0'} source-map@0.6.1: @@ -4281,7 +4284,6 @@ snapshots: dependencies: '@emnapi/wasi-threads': 1.2.1 tslib: 2.8.1 - optional: true '@emnapi/core@1.4.5': dependencies: @@ -4291,7 +4293,6 @@ snapshots: '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 - optional: true '@emnapi/runtime@1.4.5': dependencies: @@ -4304,7 +4305,6 @@ snapshots: '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 - optional: true '@esbuild/aix-ppc64@0.28.1': optional: true @@ -4696,8 +4696,8 @@ snapshots: '@napi-rs/wasm-runtime@0.2.4': dependencies: - '@emnapi/core': 1.4.5 - '@emnapi/runtime': 1.4.5 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 '@tybys/wasm-util': 0.9.0 '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': @@ -4839,7 +4839,7 @@ snapshots: '@npmcli/query@4.0.1': dependencies: - postcss-selector-parser: 7.1.4 + postcss-selector-parser: 7.1.6 '@npmcli/redact@3.2.2': {} @@ -4854,45 +4854,45 @@ snapshots: proc-log: 6.1.0 which: 6.0.1 - '@nx/devkit@22.7.7(nx@22.7.7)': + '@nx/devkit@22.7.7(nx@22.7.10)': dependencies: '@zkochan/js-yaml': 0.0.7 ejs: 5.0.1 enquirer: 2.3.6 minimatch: 10.2.5 - nx: 22.7.7 + nx: 22.7.10 semver: 7.7.2 tslib: 2.8.1 yargs-parser: 21.1.1 - '@nx/nx-darwin-arm64@22.7.7': + '@nx/nx-darwin-arm64@22.7.10': optional: true - '@nx/nx-darwin-x64@22.7.7': + '@nx/nx-darwin-x64@22.7.10': optional: true - '@nx/nx-freebsd-x64@22.7.7': + '@nx/nx-freebsd-x64@22.7.10': optional: true - '@nx/nx-linux-arm-gnueabihf@22.7.7': + '@nx/nx-linux-arm-gnueabihf@22.7.10': optional: true - '@nx/nx-linux-arm64-gnu@22.7.7': + '@nx/nx-linux-arm64-gnu@22.7.10': optional: true - '@nx/nx-linux-arm64-musl@22.7.7': + '@nx/nx-linux-arm64-musl@22.7.10': optional: true - '@nx/nx-linux-x64-gnu@22.7.7': + '@nx/nx-linux-x64-gnu@22.7.10': optional: true - '@nx/nx-linux-x64-musl@22.7.7': + '@nx/nx-linux-x64-musl@22.7.10': optional: true - '@nx/nx-win32-arm64-msvc@22.7.7': + '@nx/nx-win32-arm64-msvc@22.7.10': optional: true - '@nx/nx-win32-x64-msvc@22.7.7': + '@nx/nx-win32-x64-msvc@22.7.10': optional: true '@octokit/auth-token@4.0.0': {} @@ -5498,9 +5498,9 @@ snapshots: dependencies: possible-typed-array-names: 1.1.0 - axios@1.18.0(supports-color@7.2.0): + axios@1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0): dependencies: - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.6 https-proxy-agent: 5.0.1(supports-color@7.2.0) proxy-from-env: 2.1.0 @@ -5541,7 +5541,7 @@ snapshots: dependencies: balanced-match: 1.0.2 - brace-expansion@5.0.9: + brace-expansion@5.0.12: dependencies: balanced-match: 4.0.4 @@ -6296,7 +6296,9 @@ snapshots: flatted@3.4.3: {} - follow-redirects@1.16.0: {} + follow-redirects@1.16.0(debug@4.4.3(supports-color@7.2.0)): + optionalDependencies: + debug: 4.4.3(supports-color@7.2.0) for-each@0.3.5: dependencies: @@ -6881,7 +6883,7 @@ snapshots: '@npmcli/arborist': 9.1.6 '@npmcli/package-json': 7.0.2 '@npmcli/run-script': 10.0.3 - '@nx/devkit': 22.7.7(nx@22.7.7) + '@nx/devkit': 22.7.7(nx@22.7.10) '@octokit/plugin-enterprise-rest': 6.0.1 '@octokit/rest': 20.1.2 aproba: 2.0.0 @@ -6919,7 +6921,7 @@ snapshots: npm-package-arg: 13.0.1 npm-packlist: 10.0.3 npm-registry-fetch: 19.1.0 - nx: 22.7.7 + nx: 22.7.10 p-map: 4.0.0 p-map-series: 2.1.0 p-pipe: 3.1.0 @@ -6949,7 +6951,6 @@ snapshots: - '@swc/core' - '@types/node' - babel-plugin-macros - - debug - supports-color levn@0.4.1: @@ -7075,7 +7076,7 @@ snapshots: dependencies: '@babel/parser': 7.29.8 '@babel/types': 7.29.8 - source-map-js: 1.2.1 + source-map-js: 1.2.2 make-dir@4.0.0: dependencies: @@ -7159,11 +7160,11 @@ snapshots: minimatch@10.2.5: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@10.2.6: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimatch@3.1.4: dependencies: @@ -7179,7 +7180,7 @@ snapshots: minimatch@9.0.7: dependencies: - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 minimist-options@4.1.0: dependencies: @@ -7357,7 +7358,7 @@ snapshots: dependencies: path-key: 4.0.0 - nx@22.7.7: + nx@22.7.10: dependencies: '@emnapi/core': 1.4.5 '@emnapi/runtime': 1.4.5 @@ -7367,16 +7368,17 @@ snapshots: '@tybys/wasm-util': 0.9.0 '@yarnpkg/lockfile': 1.1.0 '@zkochan/js-yaml': 0.0.7 + agent-base: 6.0.2(supports-color@7.2.0) ansi-colors: 4.1.3 ansi-regex: 5.0.1 ansi-styles: 4.3.0 argparse: 2.0.1 asynckit: 0.4.0 - axios: 1.18.0(supports-color@7.2.0) + axios: 1.20.0(debug@4.4.3(supports-color@7.2.0))(supports-color@7.2.0) balanced-match: 4.0.3 base64-js: 1.5.1 bl: 4.1.0 - brace-expansion: 5.0.9 + brace-expansion: 5.0.12 buffer: 5.7.1 call-bind-apply-helpers: 1.0.2 chalk: 4.1.2 @@ -7387,6 +7389,7 @@ snapshots: color-convert: 2.0.1 color-name: 1.1.4 combined-stream: 1.0.8 + debug: 4.4.3(supports-color@7.2.0) defaults: 1.0.4 define-lazy-prop: 2.0.0 delayed-stream: 1.0.0 @@ -7405,7 +7408,7 @@ snapshots: escape-string-regexp: 1.0.5 figures: 3.2.0 flat: 5.0.2 - follow-redirects: 1.16.0 + follow-redirects: 1.16.0(debug@4.4.3(supports-color@7.2.0)) form-data: 4.0.6 fs-constants: 1.0.0 function-bind: 1.1.2 @@ -7417,6 +7420,7 @@ snapshots: has-symbols: 1.1.0 has-tostringtag: 1.0.2 hasown: 2.0.4 + https-proxy-agent: 5.0.1(supports-color@7.2.0) ieee754: 1.2.1 ignore: 7.0.5 inherits: 2.0.4 @@ -7435,6 +7439,7 @@ snapshots: mimic-fn: 2.1.0 minimatch: 10.2.5 minimist: 1.2.8 + ms: 2.1.3 npm-run-path: 4.0.1 once: 1.4.0 onetime: 5.1.2 @@ -7450,7 +7455,7 @@ snapshots: safe-buffer: 5.2.1 semver: 7.7.4 signal-exit: 3.0.7 - smol-toml: 1.7.1 + smol-toml: 1.9.0 string-width: 4.2.3 string_decoder: 1.3.0 strip-ansi: 6.0.1 @@ -7470,18 +7475,16 @@ snapshots: yargs: 17.7.2 yargs-parser: 21.1.1 optionalDependencies: - '@nx/nx-darwin-arm64': 22.7.7 - '@nx/nx-darwin-x64': 22.7.7 - '@nx/nx-freebsd-x64': 22.7.7 - '@nx/nx-linux-arm-gnueabihf': 22.7.7 - '@nx/nx-linux-arm64-gnu': 22.7.7 - '@nx/nx-linux-arm64-musl': 22.7.7 - '@nx/nx-linux-x64-gnu': 22.7.7 - '@nx/nx-linux-x64-musl': 22.7.7 - '@nx/nx-win32-arm64-msvc': 22.7.7 - '@nx/nx-win32-x64-msvc': 22.7.7 - transitivePeerDependencies: - - debug + '@nx/nx-darwin-arm64': 22.7.10 + '@nx/nx-darwin-x64': 22.7.10 + '@nx/nx-freebsd-x64': 22.7.10 + '@nx/nx-linux-arm-gnueabihf': 22.7.10 + '@nx/nx-linux-arm64-gnu': 22.7.10 + '@nx/nx-linux-arm64-musl': 22.7.10 + '@nx/nx-linux-x64-gnu': 22.7.10 + '@nx/nx-linux-x64-musl': 22.7.10 + '@nx/nx-win32-arm64-msvc': 22.7.10 + '@nx/nx-win32-x64-msvc': 22.7.10 object-inspect@1.13.4: {} @@ -7739,7 +7742,7 @@ snapshots: possible-typed-array-names@1.1.0: {} - postcss-selector-parser@7.1.4: + postcss-selector-parser@7.1.6: dependencies: cssesc: 3.0.0 util-deprecate: 1.0.2 @@ -7748,7 +7751,7 @@ snapshots: dependencies: nanoid: 3.3.19 picocolors: 1.1.1 - source-map-js: 1.2.1 + source-map-js: 1.2.2 prelude-ls@1.2.1: {} @@ -7763,7 +7766,7 @@ snapshots: '@jest/schemas': 30.4.1 ansi-styles: 5.2.0 react-is-18: react-is@18.3.1 - react-is-19: react-is@19.3.0 + react-is-19: react-is@19.2.8 proc-log@5.0.0: {} @@ -7798,7 +7801,7 @@ snapshots: react-is@18.3.1: {} - react-is@19.3.0: {} + react-is@19.2.8: {} read-cmd-shim@4.0.0: {} @@ -8093,7 +8096,7 @@ snapshots: smart-buffer@4.2.0: {} - smol-toml@1.7.1: {} + smol-toml@1.9.0: {} socks-proxy-agent@8.0.5: dependencies: @@ -8108,7 +8111,7 @@ snapshots: ip-address: 10.7.2 smart-buffer: 4.2.0 - source-map-js@1.2.1: {} + source-map-js@1.2.2: {} source-map@0.6.1: {} diff --git a/tests/e2e/13-config-variations.test.ts b/tests/e2e/13-config-variations.test.ts index b091bb6..1d14248 100644 --- a/tests/e2e/13-config-variations.test.ts +++ b/tests/e2e/13-config-variations.test.ts @@ -1,5 +1,6 @@ -import { describe, test, expect, beforeAll } from 'vitest'; +import { describe, test, expect, beforeAll, vi } from 'vitest'; import path from 'node:path'; +import { ConfigError, resolveContractPaths } from '../../packages/cli/src/config/loader.js'; import { createTempRepo, copyFixture, @@ -172,6 +173,102 @@ ai: } }); + test.each([ + ['reported deeply nested braces', '{'.repeat(4500) + 'x' + '}'.repeat(4500) + '*', '1024 characters'], + ['oversized flat glob', 'x'.repeat(1024) + '*', '1024 characters'], + ['nested braces', '{'.repeat(33) + 'x' + '}'.repeat(33) + '*', '32 levels'], + ['nested parentheses', '('.repeat(33) + 'x' + ')'.repeat(33) + '*', '32 levels'], + ['mixed nesting', '{('.repeat(17) + 'x' + ')}'.repeat(17) + '*', '32 levels'], + ['quoted closing braces', '{"}"'.repeat(33) + '*', '32 levels'], + ['character-class closing braces', '{[}]'.repeat(33) + '*', '32 levels'], + ['escaped closing braces', '{\\}'.repeat(33) + '*', '32 levels'], + ['mismatched closing braces', '(}'.repeat(33) + '*', '32 levels'], + ])('rejects %s as a controlled configuration error', (_name, pattern, limit) => { + const { dir, cleanup } = createTempRepo(); + try { + writeFile(dir, 'contractual.yaml', JSON.stringify({ + contracts: [{ name: 'api', type: 'openapi', path: pattern }], + })); + + const result = run('status', dir, { expectFail: true, timeout: 5000 }); + expect(result.exitCode).toBe(1); + expect(result.stderr).toContain('Glob for contract "api" exceeds'); + expect(result.stderr).toContain(limit); + expect(result.stderr).not.toContain('Maximum call stack'); + expect(result.stderr).not.toContain(pattern); + } finally { + cleanup(); + } + }); + + test.each([ + 'schemas/{order,other}.j*', + 'schemas/{order,{other,third}}.j*', + 'schemas/@(order|other).j*', + 'schemas/[o]rder.j*', + ])('preserves normal glob matching: %s', (pattern) => { + const { dir, cleanup } = createTempRepo(); + try { + copyFixture('json-schema/order-base.json', path.join(dir, 'schemas/order.json')); + const contracts = resolveContractPaths({ + contracts: [{ name: 'order', type: 'json-schema', path: pattern }], + }, dir); + expect(contracts).toHaveLength(1); + expect(contracts[0].absolutePath).toBe(path.join(dir, 'schemas/order.json')); + } finally { + cleanup(); + } + }); + + test('bounds the full resolved pattern and accepts the exact length limit', () => { + const { dir, cleanup } = createTempRepo(); + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + const prefixLength = path.resolve(dir).length + path.sep.length; + const pattern = 'x'.repeat(1024 - prefixLength - 1) + '*'; + const config = { contracts: [{ name: 'api', type: 'openapi' as const, path: pattern }] }; + expect(() => resolveContractPaths(config, dir)).not.toThrow(); + config.contracts[0].path += '*'; + expect(() => resolveContractPaths(config, dir)).toThrow(ConfigError); + expect(() => resolveContractPaths(config, dir)).toThrow('1024 characters'); + } finally { + warning.mockRestore(); + cleanup(); + } + }); + + test.each([ + ['exact nesting limit', '{'.repeat(32) + 'x' + '}'.repeat(32) + '*'], + ['escaped braces', '\\{'.repeat(40) + 'x' + '\\}'.repeat(40) + '*'], + ['quoted braces', '"' + '{'.repeat(40) + 'x' + '}'.repeat(40) + '"*'], + ['character-class braces', '[' + '{'.repeat(40) + ']' + '*'], + ])('accepts %s without treating literals as nesting', (_name, pattern) => { + const { dir, cleanup } = createTempRepo(); + const warning = vi.spyOn(console, 'warn').mockImplementation(() => {}); + try { + expect(() => resolveContractPaths({ + contracts: [{ name: 'api', type: 'openapi', path: pattern }], + }, dir)).not.toThrow(); + } finally { + warning.mockRestore(); + cleanup(); + } + }); + + test('leaves literal paths with braces unchanged', () => { + const { dir, cleanup } = createTempRepo(); + try { + const relativePath = 'schemas/' + '{'.repeat(40) + 'order' + '}'.repeat(40) + '.json'; + copyFixture('json-schema/order-base.json', path.join(dir, relativePath)); + const contracts = resolveContractPaths({ + contracts: [{ name: 'order', type: 'json-schema', path: relativePath }], + }, dir); + expect(contracts[0].absolutePath).toBe(path.join(dir, relativePath)); + } finally { + cleanup(); + } + }); + test('contract with all optional fields specified', () => { const { dir, cleanup } = createTempRepo(); try {