diff --git a/.claude/skills/update-ast-cli/SKILL.md b/.claude/skills/update-ast-cli/SKILL.md new file mode 100644 index 00000000..d57f4809 --- /dev/null +++ b/.claude/skills/update-ast-cli/SKILL.md @@ -0,0 +1,224 @@ +--- +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 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 + +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. If something fails, see +Troubleshooting at the end before improvising. + +## 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. `gh` is authenticated, so this +avoids anonymous API rate limits: + +```bash +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`. +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 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 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 --out checkmarx-ast-cli.checksums +``` + +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: + +```bash +printf '\n' > checkmarx-ast-cli.version +``` + +## 5. Bump the package version + +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 +npm version --no-git-tag-version # e.g. 1.0.45 +``` + +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 ' +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 five keys are checked +by name because `CxInstaller` looks them up that way 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 +``` + +`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 + +```bash +git push -u origin feature/update_ +``` + +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 \ + -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. +- Release notes reviewed: +``` + +## 9. Report + +Give the user the PR URL, the old and new CLI and package versions, and +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 new file mode 100644 index 00000000..ae13f198 --- /dev/null +++ b/.claude/skills/update-ast-cli/scripts/fetch-checksums.sh @@ -0,0 +1,207 @@ +#!/usr/bin/env bash +# Download the five Checkmarx AST CLI archives for a given version, compute +# 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 [--out FILE] [--download-dir DIR] [--skip-crosscheck] +# 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. +# --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). +# +# 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="" +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}" + +# 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)" + CLEANUP_DIR="$DL_DIR" +fi +mkdir -p "$DL_DIR" +# Only remove what we created; a user-supplied directory is theirs to keep. +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 + sum="$(sha256sum "$1" | awk '{print $1}')" + elif command -v shasum >/dev/null 2>&1; then + 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 +# _ 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=() + +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 + if ! fetch "$URL" "$OUT"; then + echo "ERROR: download failed: $URL" >&2 + exit 1 + fi + 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; 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 fetch "$GH_URL" "$GH_SUMS"; then + 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 + 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 + 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 +fi + +emit_json() { + echo "{" + 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 + +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 diff --git a/.gitignore b/.gitignore index 866d991c..5bac3a56 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ dist .DS_Store .npmrc .idea -/.claude +/.claude/* +!/.claude/skills/ /.vscode 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",