From a0a6594b20682c8ee092db99e0c64bb066ef5658 Mon Sep 17 00:00:00 2001 From: Anurag Dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:42:37 +0530 Subject: [PATCH 1/4] Update checkmarx-ast-cli to 2.3.69 * bump checkmarx-ast-cli.version to 2.3.69 * refresh SHA-256 checksums for all five CLI archives * bump package version to 1.0.44 Co-Authored-By: Claude Fable 5.1 --- checkmarx-ast-cli.checksums | 10 +++++----- checkmarx-ast-cli.version | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/checkmarx-ast-cli.checksums b/checkmarx-ast-cli.checksums index 51bc9b0a..9bd5e0ff 100644 --- a/checkmarx-ast-cli.checksums +++ b/checkmarx-ast-cli.checksums @@ -1,7 +1,7 @@ { - "windows_x64": "94f64a509c5636b0d7616fdd8511356259a4205f50a03ea4218d50f9e4490fb8", - "darwin_x64": "75d539854f274931b8c664c9beb5d77a60b1f33c0ae17f65dfded42059bae128", - "linux_x64": "ade477cd8c57e7caedd26ba21a9fa61dda9c6706e5d4ab57e0878d20dcaad2ca", - "linux_arm64": "e351e016d52acbd502a2f75cddc4943bbbc887c1174898d11fcb19a41e6d6e6c", - "linux_armv6": "0b16db71b3b7d3457b28908eec5502b09d4e8eafee53aae0862358b3a7f86f00" + "windows_x64": "5c8ac256467278cb0da98db98f33f5ec51d707f45f7e63636a8c0e577358ebe4", + "darwin_x64": "3f7314f9a8c766e11bac53705573472fd356ded7fe58036ed702a54a82a4303d", + "linux_x64": "91d886150114619ed1465c195660a882640b24ae56659278b6ad1498f2bc66a5", + "linux_arm64": "3635efae714b80c29549162f0bba7305e9bb012897369ace043e3a1c1aa03ee5", + "linux_armv6": "f89896b28cb035838a8889a0b8421691e88ec1471db7a8d1b7ca83e3f2dede40" } diff --git a/checkmarx-ast-cli.version b/checkmarx-ast-cli.version index 53c4a94c..175f3771 100644 --- a/checkmarx-ast-cli.version +++ b/checkmarx-ast-cli.version @@ -1 +1 @@ -2.3.67 +2.3.69 diff --git a/package-lock.json b/package-lock.json index c823fca8..299c8c50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@Checkmarx/ast-cli-javascript-wrapper-runtime-cli", - "version": "1.0.43", + "version": "1.0.44", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@Checkmarx/ast-cli-javascript-wrapper-runtime-cli", - "version": "1.0.43", + "version": "1.0.44", "license": "ISC", "dependencies": { "async-mutex": "^0.5.0", diff --git a/package.json b/package.json index e8d8c05d..060c1fc5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@Checkmarx/ast-cli-javascript-wrapper-runtime-cli", - "version": "1.0.43", + "version": "1.0.44", "description": "AST CLI Javascript wrapper runtime CLI", "main": "dist/main/wrapper/CxWrapper.js", "typings": "dist/main/wrapper/CxWrapper.d.ts", From e9197de33012f9347dc76e6e4ddaf751ad840be7 Mon Sep 17 00:00:00 2001 From: Anurag Dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:55:09 +0530 Subject: [PATCH 2/4] Add update-ast-cli skill for CLI version upgrades * add .claude/skills/update-ast-cli with SKILL.md and checksum script * stop ignoring .claude so project skills are shared; keep settings.local.json ignored Co-Authored-By: Claude Fable 5.1 --- .claude/skills/update-ast-cli/SKILL.md | 207 ++++++++++++++++++ .../update-ast-cli/scripts/fetch-checksums.sh | 74 +++++++ .gitignore | 2 +- 3 files changed, 282 insertions(+), 1 deletion(-) create mode 100644 .claude/skills/update-ast-cli/SKILL.md create mode 100644 .claude/skills/update-ast-cli/scripts/fetch-checksums.sh diff --git a/.claude/skills/update-ast-cli/SKILL.md b/.claude/skills/update-ast-cli/SKILL.md new file mode 100644 index 00000000..6b76063a --- /dev/null +++ b/.claude/skills/update-ast-cli/SKILL.md @@ -0,0 +1,207 @@ +--- +name: update-ast-cli +description: Upgrade the bundled Checkmarx AST CLI in this repo to a new release end to end - resolve the latest (or a given) ast-cli version, create the feature/update_ branch, refresh checkmarx-ast-cli.version and checkmarx-ast-cli.checksums with verified SHA-256 hashes, bump package.json / package-lock.json, make a signed commit, push, and open the PR into main. Use this whenever the user mentions a new ast-cli release, says "update/upgrade/bump the CLI", names an ast-cli version like 2.3.70, asks to refresh the CLI checksums or version file, or asks for the "update CLI" branch/PR - even if they only ask for one of those steps, because the steps depend on each other. +--- + +# Update the Checkmarx AST CLI version + +This repo pins a specific ast-cli release in two files, verifies downloaded +binaries against the pinned checksums at runtime (`CxInstaller`), and ships +each CLI bump as its own wrapper release. A CLI update is therefore a fixed, +four-file change plus a branch and a PR. Automation exists in +`.github/workflows/update-cli.yml`; this skill is the manual equivalent and +deliberately mirrors it so the two never drift. + +Files touched, and nothing else: + +| File | Change | +|---|---| +| `checkmarx-ast-cli.version` | the new CLI version, single line | +| `checkmarx-ast-cli.checksums` | SHA-256 for the five release archives | +| `package.json` | patch bump of the wrapper version | +| `package-lock.json` | the two root `version` fields only | + +Work through the steps in order. Tell the user what you are doing in a line +as you go, and finish with the PR URL. + +## 1. Preflight + +Start from an up-to-date `main` with a clean tree. Anything else risks +dragging unrelated changes into the update PR. + +```bash +git status --short # must be empty +git checkout main && git pull --ff-only +``` + +## 2. Resolve the target version + +Take the version from the user if they gave one (strip any leading `v`). +Otherwise ask GitHub for the latest release: + +```bash +curl -sL https://api.github.com/repos/Checkmarx/ast-cli/releases/latest | grep -m1 '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' +``` + +Compare with `cat checkmarx-ast-cli.version`. If they are equal, say so and +stop; there is nothing to update. Also record the current `package.json` +version so you can bump it later. + +## 3. Create the branch + +The name is `feature/update_`, for example `feature/update_2.3.69`. +This matches the branch the workflow would create, so people can find either +kind of update PR the same way. + +```bash +git checkout -b feature/update_ +``` + +If the branch already exists locally or on origin, someone (or the workflow) +has started this update. Check out that branch and inspect it instead of +making a competing one. + +## 4. Refresh the checksums + +Run the bundled script. It downloads all five archives from +`download.checkmarx.com`, hashes them, cross-checks every hash against the +`ast-cli__checksums.txt` published on the GitHub release, and prints +the JSON for the checksums file. Write that output over the file: + +```bash +bash .claude/skills/update-ast-cli/scripts/fetch-checksums.sh > checkmarx-ast-cli.checksums +``` + +The script exits non-zero on a failed download or a hash mismatch. Do not +work around either: a mismatch between the download mirror and the GitHub +release is exactly what the runtime checksum verification exists to catch. +The archives total roughly 230 MB, so allow a few minutes. Progress goes to +stderr, only the JSON goes to stdout, so the redirect above is safe. + +Then write the version file: + +```bash +printf '\n' > checkmarx-ast-cli.version +``` + +## 5. Bump the package version and lockfile + +Increment the patch component of `version` in `package.json` (1.0.43 to +1.0.44). Each CLI bump ships as a new wrapper release, so this is required +for the release workflow to publish. + +The lockfile needs the same bump in its two root `version` fields. Prefer +letting npm do it: + +```bash +npm install --package-lock-only +``` + +This repo's `.npmrc` routes npm through the Echo Artifactory registry and +needs `ECHO_LIBRARIES_ACCESS_KEY` in the environment. Without it npm fails +with `E401`. In that case do not point npm at the public registry: the +lockfile's 590-odd `resolved` URLs all reference Echo, and a public +regeneration would rewrite every one of them into an unreviewable diff. +Since dependencies have not changed, a real regeneration would only bump the +two root versions anyway, so apply exactly that by hand: + +```bash +sed -i '0,/"version": ""/s//"version": ""/; 0,/"version": ""/s//"version": ""/' package-lock.json +``` + +Mention in your final message that you did this and why. + +## 6. Verify before committing + +```bash +node -e "JSON.parse(require('fs').readFileSync('checkmarx-ast-cli.checksums','utf8')); const l=require('./package-lock.json'); console.log(l.version, l.packages[''].version)" +git diff --stat +``` + +Expect exactly the four files listed at the top. The checksums file must +have the five keys `windows_x64`, `darwin_x64`, `linux_x64`, `linux_arm64`, +`linux_armv6`, because `CxInstaller` looks them up by that name and throws +if one is missing. + +## 7. Commit + +Stage only the four files and commit with this message shape: + +``` +Update checkmarx-ast-cli to + +* bump checkmarx-ast-cli.version to +* refresh SHA-256 checksums for all five CLI archives +* bump package version to +``` + +Append whatever attribution trailer the session requires. + +`main` has a required-signatures ruleset, so the commit has to be GPG +signed; never bypass signing with `--no-gpg-sign` or `commit.gpgsign=false`. +If git reports `gpg: skipped "": No secret key`, the usual cause on +Windows is that git is calling the gpg bundled with Git for Windows while the +key lives in Gpg4win. Confirm with: + +```bash +"/c/Program Files/GnuPG/bin/gpg.exe" --list-secret-keys --keyid-format long +``` + +If the key is listed there, sign with that gpg for this commit: + +```bash +git -c gpg.program="C:/Program Files/GnuPG/bin/gpg.exe" commit -F +``` + +and tell the user they can make it permanent with +`git config --global gpg.program "C:/Program Files/GnuPG/bin/gpg.exe"`. +If the key is not there either, stop and ask; only the user can supply it. + +## 8. Push and open the PR + +```bash +git push -u origin feature/update_ +``` + +Open the PR with the REST endpoint rather than `gh pr create`. The latter +has failed here with a bogus "No commits between main and " error +even when the compare API showed the branch ahead; the REST call with +explicit `head` and `base` works first time. + +```bash +gh api -X POST repos/Checkmarx/ast-cli-javascript-wrapper-runtime-cli/pulls \ + -f title="Update checkmarx-ast-cli to " \ + -f head="feature/update_" -f base="main" \ + -F body=@ --jq '.html_url' +``` + +Before retrying a failed create, check `gh pr list --head feature/update_` +so you never open a duplicate. + +PR body template: + +```markdown +## Summary + +Updates [checkmarx-ast-cli](https://github.com/Checkmarx/ast-cli) from to and bumps the wrapper package to . + +## Changes + +- `checkmarx-ast-cli.version`: -> +- `checkmarx-ast-cli.checksums`: refreshed SHA-256 for all five archives (windows_x64, darwin_x64, linux_x64, linux_arm64, linux_armv6) +- `package.json` / `package-lock.json`: -> + +## Verification + +- Each archive was downloaded from `download.checkmarx.com/CxOne/CLI//` and hashed locally, matching the process in `update-cli.yml`. +- All five hashes match `ast-cli__checksums.txt` on the [ GitHub release](https://github.com/Checkmarx/ast-cli/releases/tag/). +- No dependency changes; lockfile diff is the root version bump only. +``` + +Append the session's PR attribution line if one is required. + +## 9. Report + +Give the user the PR URL, the old and new CLI and package versions, and +anything you had to work around (lockfile done by hand, gpg program +override). CI and a codeowner review happen on the PR; do not merge it. diff --git a/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh new file mode 100644 index 00000000..e84f2b28 --- /dev/null +++ b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh @@ -0,0 +1,74 @@ +#!/usr/bin/env bash +# Download the five Checkmarx AST CLI archives for a given version, compute +# SHA-256 for each, cross-check against the checksums file on the GitHub +# release, and print the JSON body of checkmarx-ast-cli.checksums to stdout. +# +# Usage: fetch-checksums.sh [download-dir] +# version e.g. 2.3.69 (no leading "v") +# download-dir where archives are saved; defaults to a fresh mktemp dir +# +# Mirrors the "Download CLI and generate checksums" step in +# .github/workflows/update-cli.yml so manual and automated updates agree. +set -euo pipefail + +VERSION="${1:?usage: fetch-checksums.sh [download-dir]}" +DL_DIR="${2:-$(mktemp -d)}" +mkdir -p "$DL_DIR" + +# key,extension. Key doubles as the _ part of the archive name and +# as the property name in checkmarx-ast-cli.checksums. Order matters: it is +# the order CxInstaller expects and the order the existing file uses. +PLATFORMS=( + "windows_x64,zip" + "darwin_x64,tar.gz" + "linux_x64,tar.gz" + "linux_arm64,tar.gz" + "linux_armv6,tar.gz" +) + +declare -A SUMS +for cfg in "${PLATFORMS[@]}"; do + IFS=',' read -r KEY EXT <<< "$cfg" + URL="https://download.checkmarx.com/CxOne/CLI/${VERSION}/ast-cli_${VERSION}_${KEY}.${EXT}" + OUT="${DL_DIR}/ast-cli_${VERSION}_${KEY}.${EXT}" + echo "downloading ${KEY} ..." >&2 + if ! curl -sfL --retry 3 -o "$OUT" "$URL"; then + echo "ERROR: download failed: $URL" >&2 + exit 1 + fi + SUMS["$KEY"]="$(sha256sum "$OUT" | awk '{print $1}')" + echo " ${KEY}: ${SUMS[$KEY]} ($(stat -c %s "$OUT") bytes)" >&2 +done + +# Cross-check against the checksums the ast-cli release itself publishes. +# A mismatch means the download mirror and the GitHub release disagree, which +# must be investigated rather than committed. +GH_SUMS="${DL_DIR}/ast-cli_${VERSION}_checksums.txt" +GH_URL="https://github.com/Checkmarx/ast-cli/releases/download/${VERSION}/ast-cli_${VERSION}_checksums.txt" +if curl -sfL -o "$GH_SUMS" "$GH_URL"; then + for cfg in "${PLATFORMS[@]}"; do + IFS=',' read -r KEY EXT <<< "$cfg" + EXPECTED="$(grep -E " ast-cli_${VERSION}_${KEY}\.${EXT}\$" "$GH_SUMS" | awk '{print $1}' || true)" + if [ -z "$EXPECTED" ]; then + echo "WARN: ${KEY} not listed in GitHub release checksums; skipping cross-check" >&2 + elif [ "$EXPECTED" != "${SUMS[$KEY]}" ]; then + echo "ERROR: ${KEY} hash mismatch. local=${SUMS[$KEY]} github=${EXPECTED}" >&2 + exit 2 + fi + done + echo "all hashes match GitHub release checksums" >&2 +else + echo "WARN: could not fetch ${GH_URL}; hashes were NOT cross-checked" >&2 +fi + +# Emit JSON in the same key order as the existing checksums file. +{ + echo "{" + n=${#PLATFORMS[@]}; i=0 + for cfg in "${PLATFORMS[@]}"; do + IFS=',' read -r KEY EXT <<< "$cfg" + i=$((i+1)); sep=","; [ "$i" -eq "$n" ] && sep="" + printf ' "%s": "%s"%s\n' "$KEY" "${SUMS[$KEY]}" "$sep" + done + echo "}" +} diff --git a/.gitignore b/.gitignore index 866d991c..469fc61c 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,5 @@ dist .DS_Store .npmrc .idea -/.claude +/.claude/settings.local.json /.vscode From 40281b7c9ae29bdae2140f195cbb3240e11f75d0 Mon Sep 17 00:00:00 2001 From: Anurag Dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Thu, 29 Oct 2026 14:55:04 +0530 Subject: [PATCH 3/4] Address review feedback on update-ast-cli skill fetch-checksums.sh * fail closed: missing GitHub checksums file or entry now exits 2; explicit --skip-crosscheck flag to override * portable to bash 3.2 and BSD userland: parallel arrays instead of declare -A, shasum fallback for sha256sum, wc -c instead of stat -c * --out FILE writes atomically via temp file + mv so a failure never leaves an empty checksums file * clean up the temp download dir on exit when we created it * exact filename match with awk instead of grep -E on a dotted version SKILL.md * disable-model-invocation: true; skill runs only via /update-ast-cli * use gh api --jq for the latest tag instead of grep/sed * use npm version --no-git-tag-version for the package bump, which works offline and replaces the sed on package-lock.json * verification asserts the five checksum keys and lockfile/package version agreement * add release-notes review step * move gpg, gh and registry specifics into a generalized Troubleshooting section; drop attribution-trailer wording .gitignore * ignore /.claude/* but share /.claude/skills/ Co-Authored-By: Claude Fable 5.1 --- .claude/skills/update-ast-cli/SKILL.md | 153 ++++++++++-------- .../update-ast-cli/scripts/fetch-checksums.sh | 143 +++++++++++----- .gitignore | 3 +- 3 files changed, 192 insertions(+), 107 deletions(-) diff --git a/.claude/skills/update-ast-cli/SKILL.md b/.claude/skills/update-ast-cli/SKILL.md index 6b76063a..d57f4809 100644 --- a/.claude/skills/update-ast-cli/SKILL.md +++ b/.claude/skills/update-ast-cli/SKILL.md @@ -1,6 +1,7 @@ --- name: update-ast-cli -description: Upgrade the bundled Checkmarx AST CLI in this repo to a new release end to end - resolve the latest (or a given) ast-cli version, create the feature/update_ branch, refresh checkmarx-ast-cli.version and checkmarx-ast-cli.checksums with verified SHA-256 hashes, bump package.json / package-lock.json, make a signed commit, push, and open the PR into main. Use this whenever the user mentions a new ast-cli release, says "update/upgrade/bump the CLI", names an ast-cli version like 2.3.70, asks to refresh the CLI checksums or version file, or asks for the "update CLI" branch/PR - even if they only ask for one of those steps, because the steps depend on each other. +description: Upgrade the bundled Checkmarx AST CLI in this repo to a new release end to end - resolve the latest (or a given) ast-cli version, create the feature/update_ branch, refresh checkmarx-ast-cli.version and checkmarx-ast-cli.checksums with verified SHA-256 hashes, bump the package version, make a signed commit, push, and open the PR into main. Invoke explicitly with /update-ast-cli [version]; it creates branches, pushes, and opens PRs, so it is not auto-triggered from conversation. +disable-model-invocation: true --- # Update the Checkmarx AST CLI version @@ -22,7 +23,8 @@ Files touched, and nothing else: | `package-lock.json` | the two root `version` fields only | Work through the steps in order. Tell the user what you are doing in a line -as you go, and finish with the PR URL. +as you go, and finish with the PR URL. If something fails, see +Troubleshooting at the end before improvising. ## 1. Preflight @@ -37,16 +39,23 @@ git checkout main && git pull --ff-only ## 2. Resolve the target version Take the version from the user if they gave one (strip any leading `v`). -Otherwise ask GitHub for the latest release: +Otherwise ask GitHub for the latest release. `gh` is authenticated, so this +avoids anonymous API rate limits: ```bash -curl -sL https://api.github.com/repos/Checkmarx/ast-cli/releases/latest | grep -m1 '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' +gh api repos/Checkmarx/ast-cli/releases/latest --jq .tag_name ``` Compare with `cat checkmarx-ast-cli.version`. If they are equal, say so and stop; there is nothing to update. Also record the current `package.json` version so you can bump it later. +Skim the release notes between the current and target versions for changes +to command output or flags the wrapper parses, and mention anything relevant +in the PR body. The notes are at +`https://github.com/Checkmarx/ast-cli/releases/tag/`, or via +`gh api repos/Checkmarx/ast-cli/releases/tags/ --jq .body`. + ## 3. Create the branch The name is `feature/update_`, for example `feature/update_2.3.69`. @@ -63,20 +72,24 @@ making a competing one. ## 4. Refresh the checksums -Run the bundled script. It downloads all five archives from +Run the bundled script with `--out`. It downloads all five archives from `download.checkmarx.com`, hashes them, cross-checks every hash against the -`ast-cli__checksums.txt` published on the GitHub release, and prints -the JSON for the checksums file. Write that output over the file: +`ast-cli__checksums.txt` published on the GitHub release, and only +then replaces the checksums file (it writes to a temp file and moves it into +place, so a failure leaves the existing file untouched): ```bash -bash .claude/skills/update-ast-cli/scripts/fetch-checksums.sh > checkmarx-ast-cli.checksums +bash .claude/skills/update-ast-cli/scripts/fetch-checksums.sh --out checkmarx-ast-cli.checksums ``` -The script exits non-zero on a failed download or a hash mismatch. Do not -work around either: a mismatch between the download mirror and the GitHub -release is exactly what the runtime checksum verification exists to catch. -The archives total roughly 230 MB, so allow a few minutes. Progress goes to -stderr, only the JSON goes to stdout, so the redirect above is safe. +The script exits non-zero on a failed download, a hash mismatch, or a +cross-check it cannot complete. Do not work around any of these: the +checksums file is the repo's supply-chain pin, and a disagreement between the +download mirror and the GitHub release is exactly what runtime verification +exists to catch. `--skip-crosscheck` exists for the rare case where the +release has no checksums file; use it only when the user explicitly asks, +and say so in the PR. The archives total roughly 230 MB, so allow a few +minutes. Then write the version file: @@ -84,44 +97,42 @@ Then write the version file: printf '\n' > checkmarx-ast-cli.version ``` -## 5. Bump the package version and lockfile - -Increment the patch component of `version` in `package.json` (1.0.43 to -1.0.44). Each CLI bump ships as a new wrapper release, so this is required -for the release workflow to publish. - -The lockfile needs the same bump in its two root `version` fields. Prefer -letting npm do it: - -```bash -npm install --package-lock-only -``` +## 5. Bump the package version -This repo's `.npmrc` routes npm through the Echo Artifactory registry and -needs `ECHO_LIBRARIES_ACCESS_KEY` in the environment. Without it npm fails -with `E401`. In that case do not point npm at the public registry: the -lockfile's 590-odd `resolved` URLs all reference Echo, and a public -regeneration would rewrite every one of them into an unreviewable diff. -Since dependencies have not changed, a real regeneration would only bump the -two root versions anyway, so apply exactly that by hand: +Each CLI bump ships as a new wrapper release, so the release workflow needs a +new patch version. Let npm bump `package.json` and the two root `version` +fields of `package-lock.json` together. This works offline and never +contacts a registry, which matters because this repo's `.npmrc` points at a +private registry that may not be reachable from a developer machine: ```bash -sed -i '0,/"version": ""/s//"version": ""/; 0,/"version": ""/s//"version": ""/' package-lock.json +npm version --no-git-tag-version # e.g. 1.0.45 ``` -Mention in your final message that you did this and why. +npm also drops a trailing blank line from `package.json` if one is present; +that is expected. Do not run `npm install` for this: there are no dependency +changes, and a lockfile regenerated against the wrong registry rewrites +every `resolved` URL into an unreviewable diff. ## 6. Verify before committing ```bash -node -e "JSON.parse(require('fs').readFileSync('checkmarx-ast-cli.checksums','utf8')); const l=require('./package-lock.json'); console.log(l.version, l.packages[''].version)" +node -e ' +const fs = require("fs"); +const sums = JSON.parse(fs.readFileSync("checkmarx-ast-cli.checksums", "utf8")); +const want = ["windows_x64", "darwin_x64", "linux_x64", "linux_arm64", "linux_armv6"]; +const missing = want.filter(k => !/^[0-9a-f]{64}$/.test(sums[k] || "")); +if (missing.length) { console.error("checksums missing/invalid:", missing); process.exit(1); } +const lock = require("./package-lock.json"), pkg = require("./package.json"); +if (lock.version !== pkg.version || lock.packages[""].version !== pkg.version) { console.error("lockfile version mismatch"); process.exit(1); } +console.log("ok", pkg.version, fs.readFileSync("checkmarx-ast-cli.version", "utf8").trim()); +' git diff --stat ``` -Expect exactly the four files listed at the top. The checksums file must -have the five keys `windows_x64`, `darwin_x64`, `linux_x64`, `linux_arm64`, -`linux_armv6`, because `CxInstaller` looks them up by that name and throws -if one is missing. +Expect exactly the four files listed at the top. The five keys are checked +by name because `CxInstaller` looks them up that way and throws if one is +missing. ## 7. Commit @@ -135,27 +146,9 @@ Update checkmarx-ast-cli to * bump package version to ``` -Append whatever attribution trailer the session requires. - -`main` has a required-signatures ruleset, so the commit has to be GPG -signed; never bypass signing with `--no-gpg-sign` or `commit.gpgsign=false`. -If git reports `gpg: skipped "": No secret key`, the usual cause on -Windows is that git is calling the gpg bundled with Git for Windows while the -key lives in Gpg4win. Confirm with: - -```bash -"/c/Program Files/GnuPG/bin/gpg.exe" --list-secret-keys --keyid-format long -``` - -If the key is listed there, sign with that gpg for this commit: - -```bash -git -c gpg.program="C:/Program Files/GnuPG/bin/gpg.exe" commit -F -``` - -and tell the user they can make it permanent with -`git config --global gpg.program "C:/Program Files/GnuPG/bin/gpg.exe"`. -If the key is not there either, stop and ask; only the user can supply it. +`main` has a required-signatures ruleset, so the commit has to be signed. +Never bypass signing with `--no-gpg-sign` or `commit.gpgsign=false`; if +signing fails, see Troubleshooting. ## 8. Push and open the PR @@ -163,10 +156,8 @@ If the key is not there either, stop and ask; only the user can supply it. git push -u origin feature/update_ ``` -Open the PR with the REST endpoint rather than `gh pr create`. The latter -has failed here with a bogus "No commits between main and " error -even when the compare API showed the branch ahead; the REST call with -explicit `head` and `base` works first time. +Open the PR with the REST endpoint and explicit `head` and `base`. See +Troubleshooting for why not `gh pr create`. ```bash gh api -X POST repos/Checkmarx/ast-cli-javascript-wrapper-runtime-cli/pulls \ @@ -196,12 +187,38 @@ Updates [checkmarx-ast-cli](https://github.com/Checkmarx/ast-cli) from - Each archive was downloaded from `download.checkmarx.com/CxOne/CLI//` and hashed locally, matching the process in `update-cli.yml`. - All five hashes match `ast-cli__checksums.txt` on the [ GitHub release](https://github.com/Checkmarx/ast-cli/releases/tag/). - No dependency changes; lockfile diff is the root version bump only. +- Release notes reviewed: ``` -Append the session's PR attribution line if one is required. - ## 9. Report Give the user the PR URL, the old and new CLI and package versions, and -anything you had to work around (lockfile done by hand, gpg program -override). CI and a codeowner review happen on the PR; do not merge it. +anything you had to work around. CI and a codeowner review happen on the PR; +do not merge it. + +## Troubleshooting + +**`gpg failed to sign the data` / `No secret key`.** Git is calling a gpg +whose keyring does not hold the configured `user.signingkey`. This is common +when more than one gpg is installed, for example the one bundled with Git for +Windows versus a separate Gpg4win or Homebrew install. Find the gpg that +lists the key (`gpg --list-secret-keys --keyid-format long` against each +candidate binary) and point git at it, either for one commit with +`git -c gpg.program= commit ...` or permanently with +`git config --global gpg.program `. If no gpg on the machine has the +key, stop and ask the user; only they can supply it. + +**`gpg: signing failed: Timeout`.** The passphrase prompt opened and was not +answered before gpg gave up. Tell the user to watch for the pinentry window +and retry the commit. + +**`gh pr create` says "No commits between main and ".** This has +happened here even when `gh api .../compare/main...` showed the +branch ahead. The REST call in step 8 with explicit refs works. Check for an +existing PR before retrying so you do not open a duplicate. + +**npm fails with `E401` / "Unable to authenticate".** A command reached the +private registry configured in `.npmrc` without the token it references in +the environment. Nothing in this skill should need the registry; if you got +here, you ran `npm install` instead of `npm version`. Restore the lockfile +with `git checkout -- package-lock.json` and use step 5 as written. diff --git a/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh index e84f2b28..95aa9bed 100644 --- a/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh +++ b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh @@ -1,34 +1,78 @@ #!/usr/bin/env bash # Download the five Checkmarx AST CLI archives for a given version, compute -# SHA-256 for each, cross-check against the checksums file on the GitHub -# release, and print the JSON body of checkmarx-ast-cli.checksums to stdout. +# SHA-256 for each, cross-check every hash against the checksums file on the +# GitHub release, and emit the JSON body of checkmarx-ast-cli.checksums. # -# Usage: fetch-checksums.sh [download-dir] -# version e.g. 2.3.69 (no leading "v") -# download-dir where archives are saved; defaults to a fresh mktemp dir +# Usage: fetch-checksums.sh [--out FILE] [--download-dir DIR] [--skip-crosscheck] +# version e.g. 2.3.69 (no leading "v") +# --out FILE write the JSON to FILE atomically (temp file + mv), so a +# failure never leaves FILE empty or half-written. +# Default: print JSON to stdout. +# --download-dir DIR keep the archives in DIR instead of a temp dir that is +# removed on exit. +# --skip-crosscheck do not fail when the GitHub checksums file is missing +# or incomplete. Off by default: the checksums file is +# this repo's supply-chain pin, so a cross-check that +# cannot be completed is treated as a failure. +# +# Exit codes: 0 ok, 1 usage/download error, 2 cross-check failure. # # Mirrors the "Download CLI and generate checksums" step in # .github/workflows/update-cli.yml so manual and automated updates agree. +# Written for bash 3.2+ and BSD or GNU userland (macOS and Linux both ship +# a darwin/linux CLI, so contributors on either must be able to run this). set -euo pipefail -VERSION="${1:?usage: fetch-checksums.sh [download-dir]}" -DL_DIR="${2:-$(mktemp -d)}" +VERSION="" +OUT_FILE="" +DL_DIR="" +SKIP_CROSSCHECK=0 +while [ $# -gt 0 ]; do + case "$1" in + --out) OUT_FILE="${2:?--out needs a file}"; shift 2 ;; + --download-dir) DL_DIR="${2:?--download-dir needs a directory}"; shift 2 ;; + --skip-crosscheck) SKIP_CROSSCHECK=1; shift ;; + -h|--help) sed -n '2,/^$/p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;; + -*) echo "ERROR: unknown option $1" >&2; exit 1 ;; + *) if [ -z "$VERSION" ]; then VERSION="$1"; shift; else echo "ERROR: unexpected argument $1" >&2; exit 1; fi ;; + esac +done +if [ -z "$VERSION" ]; then + echo "usage: fetch-checksums.sh [--out FILE] [--download-dir DIR] [--skip-crosscheck]" >&2 + exit 1 +fi +VERSION="${VERSION#v}" + +CLEANUP_DIR="" +if [ -z "$DL_DIR" ]; then + DL_DIR="$(mktemp -d)" + CLEANUP_DIR="$DL_DIR" +fi mkdir -p "$DL_DIR" +# Only remove what we created; a user-supplied directory is theirs to keep. +trap '[ -n "$CLEANUP_DIR" ] && rm -rf "$CLEANUP_DIR"' EXIT + +sha256_of() { + if command -v sha256sum >/dev/null 2>&1; then + sha256sum "$1" | awk '{print $1}' + elif command -v shasum >/dev/null 2>&1; then + shasum -a 256 "$1" | awk '{print $1}' + else + echo "ERROR: need sha256sum or shasum on PATH" >&2 + exit 1 + fi +} -# key,extension. Key doubles as the _ part of the archive name and -# as the property name in checkmarx-ast-cli.checksums. Order matters: it is -# the order CxInstaller expects and the order the existing file uses. -PLATFORMS=( - "windows_x64,zip" - "darwin_x64,tar.gz" - "linux_x64,tar.gz" - "linux_arm64,tar.gz" - "linux_armv6,tar.gz" -) +# Parallel arrays (bash 3.2 has no associative arrays). KEY doubles as the +# _ part of the archive name and as the property name in +# checkmarx-ast-cli.checksums. Order matches the existing file. +KEYS=(windows_x64 darwin_x64 linux_x64 linux_arm64 linux_armv6) +EXTS=(zip tar.gz tar.gz tar.gz tar.gz) +SUMS=() -declare -A SUMS -for cfg in "${PLATFORMS[@]}"; do - IFS=',' read -r KEY EXT <<< "$cfg" +i=0 +while [ $i -lt ${#KEYS[@]} ]; do + KEY="${KEYS[$i]}"; EXT="${EXTS[$i]}" URL="https://download.checkmarx.com/CxOne/CLI/${VERSION}/ast-cli_${VERSION}_${KEY}.${EXT}" OUT="${DL_DIR}/ast-cli_${VERSION}_${KEY}.${EXT}" echo "downloading ${KEY} ..." >&2 @@ -36,39 +80,62 @@ for cfg in "${PLATFORMS[@]}"; do echo "ERROR: download failed: $URL" >&2 exit 1 fi - SUMS["$KEY"]="$(sha256sum "$OUT" | awk '{print $1}')" - echo " ${KEY}: ${SUMS[$KEY]} ($(stat -c %s "$OUT") bytes)" >&2 + SUMS[$i]="$(sha256_of "$OUT")" + echo " ${KEY}: ${SUMS[$i]} ($(wc -c < "$OUT" | tr -d ' ') bytes)" >&2 + i=$((i + 1)) done # Cross-check against the checksums the ast-cli release itself publishes. -# A mismatch means the download mirror and the GitHub release disagree, which -# must be investigated rather than committed. +# A mismatch means the download mirror and the GitHub release disagree; a +# missing file or entry means we cannot prove they agree. Both fail unless +# the caller explicitly opted out. GH_SUMS="${DL_DIR}/ast-cli_${VERSION}_checksums.txt" GH_URL="https://github.com/Checkmarx/ast-cli/releases/download/${VERSION}/ast-cli_${VERSION}_checksums.txt" if curl -sfL -o "$GH_SUMS" "$GH_URL"; then - for cfg in "${PLATFORMS[@]}"; do - IFS=',' read -r KEY EXT <<< "$cfg" - EXPECTED="$(grep -E " ast-cli_${VERSION}_${KEY}\.${EXT}\$" "$GH_SUMS" | awk '{print $1}' || true)" + i=0 + while [ $i -lt ${#KEYS[@]} ]; do + KEY="${KEYS[$i]}"; EXT="${EXTS[$i]}" + NAME="ast-cli_${VERSION}_${KEY}.${EXT}" + # Exact filename match on the second column; no regex, so dots in the + # version are literal. + EXPECTED="$(awk -v f="$NAME" '$2 == f { print $1; exit }' "$GH_SUMS")" if [ -z "$EXPECTED" ]; then - echo "WARN: ${KEY} not listed in GitHub release checksums; skipping cross-check" >&2 - elif [ "$EXPECTED" != "${SUMS[$KEY]}" ]; then - echo "ERROR: ${KEY} hash mismatch. local=${SUMS[$KEY]} github=${EXPECTED}" >&2 + if [ "$SKIP_CROSSCHECK" -eq 1 ]; then + echo "WARN: ${NAME} not listed in GitHub release checksums (skipped by --skip-crosscheck)" >&2 + else + echo "ERROR: ${NAME} not listed in ${GH_URL}; cannot cross-check. Use --skip-crosscheck to override." >&2 + exit 2 + fi + elif [ "$EXPECTED" != "${SUMS[$i]}" ]; then + echo "ERROR: ${KEY} hash mismatch. local=${SUMS[$i]} github=${EXPECTED}" >&2 exit 2 fi + i=$((i + 1)) done echo "all hashes match GitHub release checksums" >&2 +elif [ "$SKIP_CROSSCHECK" -eq 1 ]; then + echo "WARN: could not fetch ${GH_URL}; hashes NOT cross-checked (skipped by --skip-crosscheck)" >&2 else - echo "WARN: could not fetch ${GH_URL}; hashes were NOT cross-checked" >&2 + echo "ERROR: could not fetch ${GH_URL}; cannot cross-check. Use --skip-crosscheck to override." >&2 + exit 2 fi -# Emit JSON in the same key order as the existing checksums file. -{ +emit_json() { echo "{" - n=${#PLATFORMS[@]}; i=0 - for cfg in "${PLATFORMS[@]}"; do - IFS=',' read -r KEY EXT <<< "$cfg" - i=$((i+1)); sep=","; [ "$i" -eq "$n" ] && sep="" - printf ' "%s": "%s"%s\n' "$KEY" "${SUMS[$KEY]}" "$sep" + local n=${#KEYS[@]} j=0 sep + while [ $j -lt $n ]; do + sep=","; [ $((j + 1)) -eq $n ] && sep="" + printf ' "%s": "%s"%s\n' "${KEYS[$j]}" "${SUMS[$j]}" "$sep" + j=$((j + 1)) done echo "}" } + +if [ -n "$OUT_FILE" ]; then + TMP_OUT="$(mktemp "${OUT_FILE}.XXXXXX")" + emit_json > "$TMP_OUT" + mv -f "$TMP_OUT" "$OUT_FILE" + echo "wrote ${OUT_FILE}" >&2 +else + emit_json +fi diff --git a/.gitignore b/.gitignore index 469fc61c..5bac3a56 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ dist .DS_Store .npmrc .idea -/.claude/settings.local.json +/.claude/* +!/.claude/skills/ /.vscode From ac0426b4879382867c261016be2d678d515bb93e Mon Sep 17 00:00:00 2001 From: Anurag Dalke <120229307+cx-anurag-dalke@users.noreply.github.com> Date: Thu, 29 Oct 2026 15:07:30 +0530 Subject: [PATCH 4/4] Harden fetch-checksums.sh against misuse * validate the version as strict MAJOR.MINOR.PATCH before it reaches any URL or file path; rejects traversal, URL metacharacters and malformed input * pin curl to HTTPS for the request and any redirect, cap redirects, add connect and overall timeouts * remove any pre-existing file or symlink at a download path before writing so a planted symlink is never followed * reject --out when it names a directory * verify each computed hash is 64 hex chars before emitting it * a hash mismatch is never skippable; --skip-crosscheck only covers a missing checksums file or entry, and ends with a loud UNVERIFIED banner Co-Authored-By: Claude Fable 5.1 --- .../update-ast-cli/scripts/fetch-checksums.sh | 80 +++++++++++++++++-- 1 file changed, 73 insertions(+), 7 deletions(-) diff --git a/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh index 95aa9bed..ae13f198 100644 --- a/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh +++ b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh @@ -4,7 +4,7 @@ # GitHub release, and emit the JSON body of checkmarx-ast-cli.checksums. # # Usage: fetch-checksums.sh [--out FILE] [--download-dir DIR] [--skip-crosscheck] -# version e.g. 2.3.69 (no leading "v") +# version e.g. 2.3.69 (no leading "v"); must be MAJOR.MINOR.PATCH # --out FILE write the JSON to FILE atomically (temp file + mv), so a # failure never leaves FILE empty or half-written. # Default: print JSON to stdout. @@ -21,6 +21,14 @@ # .github/workflows/update-cli.yml so manual and automated updates agree. # Written for bash 3.2+ and BSD or GNU userland (macOS and Linux both ship # a darwin/linux CLI, so contributors on either must be able to run this). +# +# Safety properties, kept deliberately: +# - only two fixed HTTPS hosts are contacted; the version is the sole +# user-controlled part of any URL and is validated as strict semver +# - curl is pinned to HTTPS for the request and any redirect, with +# timeouts, and TLS verification is never disabled +# - every file write goes to a path derived from the validated version or +# to the caller's explicit --out / --download-dir set -euo pipefail VERSION="" @@ -43,6 +51,22 @@ if [ -z "$VERSION" ]; then fi VERSION="${VERSION#v}" +# The version is interpolated into URLs and file names, so it must be exactly +# digits and dots. This rules out path traversal ("../x"), URL tricks ("?", +# "#", "@") and anything awk or the shell could misread. +case "$VERSION" in + *[!0-9.]*|.*|*.|*..*) echo "ERROR: version must look like 2.3.69, got: $VERSION" >&2; exit 1 ;; +esac +if ! printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "ERROR: version must be MAJOR.MINOR.PATCH, got: $VERSION" >&2 + exit 1 +fi + +if [ -n "$OUT_FILE" ] && [ -d "$OUT_FILE" ]; then + echo "ERROR: --out must be a file path, not a directory: $OUT_FILE" >&2 + exit 1 +fi + CLEANUP_DIR="" if [ -z "$DL_DIR" ]; then DL_DIR="$(mktemp -d)" @@ -50,17 +74,41 @@ if [ -z "$DL_DIR" ]; then fi mkdir -p "$DL_DIR" # Only remove what we created; a user-supplied directory is theirs to keep. -trap '[ -n "$CLEANUP_DIR" ] && rm -rf "$CLEANUP_DIR"' EXIT +cleanup() { if [ -n "$CLEANUP_DIR" ]; then rm -rf "$CLEANUP_DIR"; fi; } +trap cleanup EXIT + +# One curl configuration for every request: +# --proto/--proto-redir '=https' never speak anything but HTTPS, even if a +# server redirects to http:// or file:// +# -f treat HTTP errors as failures +# --max-redirs / timeouts a stalled or looping mirror fails instead +# of hanging the update +# TLS verification is curl's default and is intentionally not relaxed here. +fetch() { + # $1 = url, $2 = output path + rm -f "$2" # never follow a pre-existing symlink at the output path + curl -sfL --proto '=https' --proto-redir '=https' --max-redirs 5 \ + --connect-timeout 30 --max-time 1800 --retry 3 \ + -o "$2" "$1" +} sha256_of() { + local sum if command -v sha256sum >/dev/null 2>&1; then - sha256sum "$1" | awk '{print $1}' + sum="$(sha256sum "$1" | awk '{print $1}')" elif command -v shasum >/dev/null 2>&1; then - shasum -a 256 "$1" | awk '{print $1}' + sum="$(shasum -a 256 "$1" | awk '{print $1}')" else echo "ERROR: need sha256sum or shasum on PATH" >&2 exit 1 fi + # Refuse to emit anything that is not a well-formed SHA-256; a garbled hash + # written into the pin file would break every install at runtime. + if ! printf '%s' "$sum" | grep -Eq '^[0-9a-f]{64}$'; then + echo "ERROR: unexpected hash output for $1: $sum" >&2 + exit 1 + fi + printf '%s' "$sum" } # Parallel arrays (bash 3.2 has no associative arrays). KEY doubles as the @@ -76,7 +124,7 @@ while [ $i -lt ${#KEYS[@]} ]; do URL="https://download.checkmarx.com/CxOne/CLI/${VERSION}/ast-cli_${VERSION}_${KEY}.${EXT}" OUT="${DL_DIR}/ast-cli_${VERSION}_${KEY}.${EXT}" echo "downloading ${KEY} ..." >&2 - if ! curl -sfL --retry 3 -o "$OUT" "$URL"; then + if ! fetch "$URL" "$OUT"; then echo "ERROR: download failed: $URL" >&2 exit 1 fi @@ -89,9 +137,10 @@ done # A mismatch means the download mirror and the GitHub release disagree; a # missing file or entry means we cannot prove they agree. Both fail unless # the caller explicitly opted out. +UNVERIFIED=0 GH_SUMS="${DL_DIR}/ast-cli_${VERSION}_checksums.txt" GH_URL="https://github.com/Checkmarx/ast-cli/releases/download/${VERSION}/ast-cli_${VERSION}_checksums.txt" -if curl -sfL -o "$GH_SUMS" "$GH_URL"; then +if fetch "$GH_URL" "$GH_SUMS"; then i=0 while [ $i -lt ${#KEYS[@]} ]; do KEY="${KEYS[$i]}"; EXT="${EXTS[$i]}" @@ -102,19 +151,24 @@ if curl -sfL -o "$GH_SUMS" "$GH_URL"; then if [ -z "$EXPECTED" ]; then if [ "$SKIP_CROSSCHECK" -eq 1 ]; then echo "WARN: ${NAME} not listed in GitHub release checksums (skipped by --skip-crosscheck)" >&2 + UNVERIFIED=1 else echo "ERROR: ${NAME} not listed in ${GH_URL}; cannot cross-check. Use --skip-crosscheck to override." >&2 exit 2 fi elif [ "$EXPECTED" != "${SUMS[$i]}" ]; then + # A real mismatch is never skippable: it means the two sources disagree. echo "ERROR: ${KEY} hash mismatch. local=${SUMS[$i]} github=${EXPECTED}" >&2 exit 2 fi i=$((i + 1)) done - echo "all hashes match GitHub release checksums" >&2 + if [ "$UNVERIFIED" -eq 0 ]; then + echo "all hashes match GitHub release checksums" >&2 + fi elif [ "$SKIP_CROSSCHECK" -eq 1 ]; then echo "WARN: could not fetch ${GH_URL}; hashes NOT cross-checked (skipped by --skip-crosscheck)" >&2 + UNVERIFIED=1 else echo "ERROR: could not fetch ${GH_URL}; cannot cross-check. Use --skip-crosscheck to override." >&2 exit 2 @@ -139,3 +193,15 @@ if [ -n "$OUT_FILE" ]; then else emit_json fi + +if [ "$UNVERIFIED" -eq 1 ]; then + cat >&2 <<'BANNER' + +*** UNVERIFIED RESULT *** +One or more hashes above were NOT cross-checked against the GitHub release +because --skip-crosscheck was given. Do not commit this output without +stating that in the PR and explaining why the release checksums were +unavailable. + +BANNER +fi