diff --git a/.github/codeql-allowlist.yml b/.github/codeql-allowlist.yml deleted file mode 100644 index 2d26ba8..0000000 --- a/.github/codeql-allowlist.yml +++ /dev/null @@ -1,4 +0,0 @@ -# CodeQL finding allowlist with expiry model. -# New entries require security on-call approval. -# Expired entries fail CI via scripts/check-codeql-allowlist.sh. -allowlist: [] diff --git a/.github/deps-allowlist.yml b/.github/deps-allowlist.yml deleted file mode 100644 index ab83a63..0000000 --- a/.github/deps-allowlist.yml +++ /dev/null @@ -1,6 +0,0 @@ -# Dependency vulnerability allowlist with expiry model. -# HIGH-severity exceptions require security on-call approval. -# Mirror approved entries into .osv-scanner.toml [[IgnoredVulns]] with -# matching ignoreUntil dates. Expired entries fail CI via -# scripts/check-deps-allowlist.sh. -allowlist: [] diff --git a/.github/workflows/release-npm.yml b/.github/workflows/release-npm.yml deleted file mode 100644 index 1e7d674..0000000 --- a/.github/workflows/release-npm.yml +++ /dev/null @@ -1,338 +0,0 @@ -name: release npm - -on: - push: - tags: - - 'v*' - workflow_dispatch: - inputs: - release_ref: - description: Git ref to build and publish from. - required: true - default: refs/heads/main - type: string - release_version: - description: SemVer version for dry-run metadata (without v prefix). - required: true - default: 0.0.0-dryrun - type: string - npm_dist_tag: - description: npm dist-tag for dry-run publishes (for example next). - required: true - default: next - type: string - publish_to_npm: - description: Publish to npm with provenance (requires npm trusted publisher). - required: true - default: true - type: boolean - -permissions: - contents: write - id-token: write - -jobs: - test: - name: "IntentProof Release: Test npm Package" - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_ref || github.ref }} - - - name: Checkout spec repository - uses: actions/checkout@v6 - with: - repository: IntentProof/intentproof-spec - ref: main - path: intentproof-spec - - - uses: actions/setup-node@v6 - with: - node-version: '22' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests with coverage - env: - INTENTPROOF_SPEC_DIR: intentproof-spec - run: npm run test:coverage - - pack: - name: "IntentProof Release: Pack npm Package" - needs: test - runs-on: ubuntu-latest - outputs: - artifact_paths: ${{ steps.pack.outputs.artifact_paths }} - release_ref: ${{ steps.release.outputs.release_ref }} - release_version: ${{ steps.release.outputs.release_version }} - npm_dist_tag: ${{ steps.release.outputs.npm_dist_tag }} - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_ref || github.ref }} - - - uses: actions/setup-node@v6 - with: - node-version: '22' - cache: 'npm' - - - name: Resolve release metadata - id: release - env: - INPUT_NPM_DIST_TAG: ${{ inputs.npm_dist_tag }} - INPUT_RELEASE_REF: ${{ inputs.release_ref }} - INPUT_RELEASE_VERSION: ${{ inputs.release_version }} - run: | - set -euo pipefail - semver_tag='^refs/tags/v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$' - - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - release_ref="$INPUT_RELEASE_REF" - release_version="$INPUT_RELEASE_VERSION" - npm_dist_tag="$INPUT_NPM_DIST_TAG" - else - release_ref="${GITHUB_REF}" - release_version="${GITHUB_REF_NAME#v}" - if [[ "$release_ref" =~ $semver_tag ]]; then - if [[ "$release_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - npm_dist_tag="latest" - else - npm_dist_tag="${release_version#*-}" - npm_dist_tag="${npm_dist_tag%%.*}" - fi - else - echo "tag ref must match vMAJOR.MINOR.PATCH: ${release_ref}" >&2 - exit 1 - fi - fi - - { - echo "release_ref=${release_ref}" - echo "release_version=${release_version}" - echo "npm_dist_tag=${npm_dist_tag}" - } >> "$GITHUB_OUTPUT" - - - name: Install dependencies - run: npm ci - - - name: Sync package version - run: npm version "${{ steps.release.outputs.release_version }}" --no-git-tag-version --allow-same-version - - - name: Build package - run: npm run build - - - name: Pack npm tarball - id: pack - run: | - set -euo pipefail - npm pack - mapfile -t tarballs < <(find . -maxdepth 1 -type f -name '*.tgz' | sort) - test "${#tarballs[@]}" -eq 1 - { - echo "artifact_paths<> "$GITHUB_OUTPUT" - - - uses: actions/upload-artifact@v7 - with: - name: npm-release-package - path: ./*.tgz - - sign: - name: "IntentProof Release: Sign npm Package Tarball" - needs: pack - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v6 - with: - ref: ${{ needs.pack.outputs.release_ref }} - - - uses: actions/download-artifact@v8 - with: - name: npm-release-package - path: . - - - uses: sigstore/cosign-installer@v3 - - - uses: anchore/sbom-action/download-syft@v0 - - - name: Sign tarball and generate release metadata - env: - ARTIFACT_PATHS: ${{ needs.pack.outputs.artifact_paths }} - RELEASE_VERSION: ${{ needs.pack.outputs.release_version }} - ATTEST_TO_REKOR: ${{ github.event_name != 'workflow_dispatch' }} - run: | - set -euo pipefail - tlog_args=() - if [[ "$ATTEST_TO_REKOR" != "true" ]]; then - tlog_args+=(--tlog-upload=false) - fi - - mkdir -p release-attestations - : > release-attestations/SHA256SUMS - - while IFS= read -r artifact; do - [[ -z "$artifact" ]] && continue - test -f "$artifact" || { echo "artifact not found: $artifact" >&2; exit 1; } - - ( - cd "$(dirname "$artifact")" - sha256sum "$(basename "$artifact")" - ) >> release-attestations/SHA256SUMS - cosign sign-blob --yes \ - "${tlog_args[@]}" \ - --bundle "${artifact}.sigstore.json" \ - --output-signature "${artifact}.sig" \ - "$artifact" - - syft packages "file:${artifact}" \ - -o "spdx-json=${artifact}.spdx.json" - - syft packages . \ - -o "spdx-json=${artifact}.source.spdx.json" - - cosign attest-blob --yes \ - "${tlog_args[@]}" \ - --type spdxjson \ - --predicate "${artifact}.spdx.json" \ - --bundle "${artifact}.sbom.sigstore.json" \ - "$artifact" - - artifact_sha="$(sha256sum "$artifact" | awk '{print $1}')" - ARTIFACT_PATH="$artifact" ARTIFACT_SHA="$artifact_sha" python3 - <<'PY' - import json - import os - from pathlib import Path - - artifact_path = os.environ["ARTIFACT_PATH"] - run_id = os.environ["GITHUB_RUN_ID"] - predicate = { - "builder": { - "id": ( - f"{os.environ['GITHUB_SERVER_URL']}/" - f"{os.environ['GITHUB_REPOSITORY']}/actions/runs/{run_id}" - ) - }, - "buildType": "https://github.com/IntentProof/release-npm", - "invocation": { - "parameters": { - "releaseVersion": os.environ["RELEASE_VERSION"], - "artifactPath": artifact_path, - "subjectName": "@intentproof/sdk", - } - }, - "metadata": { - "buildInvocationId": ( - f"{run_id}-{os.environ['GITHUB_RUN_ATTEMPT']}" - ) - }, - "materials": [ - { - "uri": f"file:{artifact_path}", - "digest": {"sha256": os.environ["ARTIFACT_SHA"]}, - } - ], - } - Path(f"{artifact_path}.intoto.jsonl").write_text( - json.dumps(predicate, separators=(",", ":")) + "\n", - encoding="utf-8", - ) - PY - cosign attest-blob --yes \ - "${tlog_args[@]}" \ - --type slsaprovenance \ - --predicate "${artifact}.intoto.jsonl" \ - --bundle "${artifact}.intoto.sigstore.json" \ - "$artifact" - done <<< "$ARTIFACT_PATHS" - - cosign sign-blob --yes \ - "${tlog_args[@]}" \ - --bundle release-attestations/SHA256SUMS.sigstore.json \ - --output-signature release-attestations/SHA256SUMS.sig \ - release-attestations/SHA256SUMS - - - uses: actions/upload-artifact@v7 - with: - name: release-signing-metadata - path: | - **/*.sig - **/*.sigstore.json - **/*.spdx.json - **/*.intoto.jsonl - release-attestations/SHA256SUMS - - publish: - name: "IntentProof Release: Publish npm Package" - needs: pack - if: github.event_name != 'workflow_dispatch' || inputs.publish_to_npm - runs-on: ubuntu-latest - permissions: - contents: read - id-token: write - steps: - - uses: actions/setup-node@v6 - with: - node-version: '24' - registry-url: 'https://registry.npmjs.org' - - - uses: actions/download-artifact@v8 - with: - name: npm-release-package - path: . - - - name: Publish with npm provenance - run: | - set -euo pipefail - if [[ -n "${NPM_CONFIG_USERCONFIG:-}" && -f "$NPM_CONFIG_USERCONFIG" ]]; then - sed -i '/:_authToken=/d' "$NPM_CONFIG_USERCONFIG" - fi - npm --version - node --version - mapfile -t tarballs < <(find . -maxdepth 1 -type f -name '*.tgz' | sort) - test "${#tarballs[@]}" -eq 1 - npm publish "${tarballs[0]}" --provenance --access public \ - --tag "${{ needs.pack.outputs.npm_dist_tag }}" - - upload-release: - name: "IntentProof Release: Publish npm Release Artifacts" - needs: - - pack - - sign - - publish - if: github.event_name != 'workflow_dispatch' && needs.pack.result == 'success' && needs.sign.result == 'success' && needs.publish.result == 'success' - runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/download-artifact@v8 - with: - name: npm-release-package - path: release-package - - - uses: actions/download-artifact@v8 - with: - name: release-signing-metadata - path: release-signing-metadata - - - name: Upload tarball and signing metadata to GitHub Release - env: - GH_TOKEN: ${{ github.token }} - GH_REPO: ${{ github.repository }} - RELEASE_TAG: ${{ github.ref_name }} - run: | - set -euo pipefail - mapfile -t files < <(find release-package release-signing-metadata -type f | sort) - test "${#files[@]}" -gt 0 - - if ! gh release view "$RELEASE_TAG" >/dev/null 2>&1; then - gh release create "$RELEASE_TAG" --generate-notes - fi - gh release upload "$RELEASE_TAG" "${files[@]}" --clobber diff --git a/.github/workflows/release-signing-dry-run.yml b/.github/workflows/release-signing-dry-run.yml deleted file mode 100644 index 7da02dc..0000000 --- a/.github/workflows/release-signing-dry-run.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: release signing dry run - -on: - workflow_dispatch: - inputs: - release_version: - description: SemVer version to place in dry-run attestations. - required: true - default: 0.0.0-dryrun - type: string - release_ref: - description: Git ref to check out for the dry run. - required: true - default: refs/heads/main - type: string - -jobs: - test-package: - name: "IntentProof Release: Test npm Package" - permissions: - contents: read - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - with: - ref: ${{ inputs.release_ref }} - - - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run tests - run: npm test - - - name: Check npm package contents - run: npm pack --dry-run - - sign-npm-package: - name: "IntentProof Release: Sign npm Package" - needs: test-package - permissions: - contents: write - id-token: write - packages: write - uses: IntentProof/intentproof-tools/.github/workflows/release-build-sign.yml@317387a9724787e4ac484f39de46d7e559b6c98d - with: - artifact_kind: npm - subject_name: intentproof-sdk-node - release_version: ${{ inputs.release_version }} - release_ref: ${{ inputs.release_ref }} - npm_package_path: . - attest_to_rekor: false diff --git a/.osv-scanner.toml b/.osv-scanner.toml deleted file mode 100644 index 393cdb0..0000000 --- a/.osv-scanner.toml +++ /dev/null @@ -1,4 +0,0 @@ -# IntentProof OSV-Scanner configuration. -# CRITICAL and HIGH findings fail CI via deps-scan workflow severity filter. -# Time-bounded HIGH allowlist entries live in .github/deps-allowlist.yml and -# must be mirrored here under [[IgnoredVulns]] with matching ignoreUntil dates. diff --git a/package.json b/package.json index c3b8470..165b450 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,7 @@ "scripts": { "build": "tsc -p tsconfig.json", "prepublishOnly": "npm run build", - "test": "TS_NODE_PREFER_TS_EXTS=true node -r ts-node/register --test tests/*.test.ts", - "test:coverage": "npm run build && node --experimental-test-coverage --test-coverage-include='dist/**/*.js' --test-coverage-exclude='dist/index.js' --test-coverage-lines=95 --test-coverage-branches=95 --test-coverage-functions=95 -r ts-node/register -r ./tests/preload-dist.cjs --test tests/*.test.ts" + "test": "TS_NODE_PREFER_TS_EXTS=true node -r ts-node/register --test tests/*.test.ts" }, "repository": { "type": "git", diff --git a/scripts/check-allowlist-expiry.sh b/scripts/check-allowlist-expiry.sh deleted file mode 100755 index db93e7c..0000000 --- a/scripts/check-allowlist-expiry.sh +++ /dev/null @@ -1,111 +0,0 @@ -#!/usr/bin/env bash -# Validate allowlist YAML expiry dates (minimal parser, no PyYAML). -# Usage: check-allowlist-expiry.sh [id-field...] -set -euo pipefail - -if [[ $# -lt 1 || -z "${1:-}" ]]; then - echo "usage: check-allowlist-expiry.sh [id-field...]" >&2 - exit 1 -fi - -ALLOWLIST_FILE="$1" -shift -ID_FIELDS=("$@") -if ((${#ID_FIELDS[@]} == 0)); then - ID_FIELDS=(rule_id) -fi - -if [[ ! -f "$ALLOWLIST_FILE" ]]; then - echo "No allowlist file at $ALLOWLIST_FILE; skipping expiry check." - exit 0 -fi - -if ! command -v python3 >/dev/null 2>&1; then - echo "python3 is required to validate $ALLOWLIST_FILE" >&2 - exit 1 -fi - -python3 - "$ALLOWLIST_FILE" "${ID_FIELDS[*]}" <<'PY' -import datetime -import re -import sys - -path = sys.argv[1] -id_fields = sys.argv[2].split() -patterns = { - "rule_id": re.compile(r"rule_id:\s*(\S+)"), - "cve_id": re.compile(r"cve_id:\s*(\S+)"), - "id": re.compile(r"(?:^|\s)id:\s*(\S+)"), -} - - -def extract_id(text, anchored=False): - for field in id_fields: - pattern = patterns.get(field) - if pattern is None: - continue - match = pattern.match(text) if anchored else pattern.search(text) - if match: - return match.group(1) - return None - - -text = open(path, encoding="utf-8").read() -entries = [] -current = None -for line in text.splitlines(): - stripped = line.strip() - if stripped.startswith("- "): - if current is not None: - entries.append(current) - current = {} - item = stripped[2:].strip() - entry_id = extract_id(item, anchored=False) - if entry_id: - current["id"] = entry_id - match = re.search(r"expires:\s*(\S+)", item) - if match: - current["expires"] = match.group(1) - continue - if current is None: - continue - match = re.match(r"expires:\s*(\S+)", stripped) - if match: - current["expires"] = match.group(1) - entry_id = extract_id(stripped, anchored=True) - if entry_id: - current["id"] = entry_id -if current is not None: - entries.append(current) - -today = datetime.date.today() -expired = [] -for idx, entry in enumerate(entries): - expires_raw = entry.get("expires") - if not expires_raw: - print( - f"{path}: allowlist[{idx}] missing expires date " - "(required for security on-call approval model)", - file=sys.stderr, - ) - sys.exit(1) - try: - expires = datetime.date.fromisoformat(str(expires_raw)) - except ValueError: - print( - f"{path}: allowlist[{idx}] has invalid expires date: {expires_raw!r}", - file=sys.stderr, - ) - sys.exit(1) - if expires < today: - entry_id = entry.get("id", "") - expired.append(f"{entry_id} (expired {expires.isoformat()})") - -if expired: - print("Allowlist expired; contact security on-call to extend or remove:", file=sys.stderr) - for item in expired: - print(f" - {item}", file=sys.stderr) - sys.exit(1) - -print(f"PASS: {len(entries)} allowlist entries are current.") -PY diff --git a/scripts/check-codeql-allowlist.sh b/scripts/check-codeql-allowlist.sh deleted file mode 100755 index 9e58438..0000000 --- a/scripts/check-codeql-allowlist.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Validate .github/codeql-allowlist.yml expiry dates. -exec "$(dirname "$0")/check-allowlist-expiry.sh" "${1:-.github/codeql-allowlist.yml}" rule_id diff --git a/scripts/check-deps-allowlist.sh b/scripts/check-deps-allowlist.sh deleted file mode 100755 index 698999e..0000000 --- a/scripts/check-deps-allowlist.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/usr/bin/env bash -# Validate .github/deps-allowlist.yml expiry dates. -exec "$(dirname "$0")/check-allowlist-expiry.sh" "${1:-.github/deps-allowlist.yml}" rule_id cve_id id diff --git a/scripts/run-osv-scanner-gate.sh b/scripts/run-osv-scanner-gate.sh deleted file mode 100755 index 8d1cece..0000000 --- a/scripts/run-osv-scanner-gate.sh +++ /dev/null @@ -1,101 +0,0 @@ -#!/usr/bin/env bash -# Run OSV-Scanner and fail only on CRITICAL or HIGH findings. -# MEDIUM and LOW are reported but non-blocking per SECURITY-PROCESS.md. -set -euo pipefail - -ROOT="${1:-.}" -CONFIG="${2:-.osv-scanner.toml}" -if (( $# > 2 )); then - EXTRA_LOCKFILES=("${@:3}") -else - EXTRA_LOCKFILES=() -fi -OSV_VERSION="${OSV_SCANNER_VERSION:-v2.2.2}" - -if ! command -v osv-scanner >/dev/null 2>&1; then - arch=linux_amd64 - case "$(uname -m)" in - aarch64 | arm64) arch=linux_arm64 ;; - x86_64 | amd64) arch=linux_amd64 ;; - esac - tmp="$(mktemp)" - curl -sSfL \ - "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_${arch}" \ - -o "$tmp" - chmod +x "$tmp" - sudo install -m 755 "$tmp" /usr/local/bin/osv-scanner - rm -f "$tmp" -fi - -args=(scan source --format=table --no-call-analysis=all --allow-no-lockfiles) -if [[ -f "$CONFIG" ]]; then - args+=(--config="$CONFIG") -fi - -if ((${#EXTRA_LOCKFILES[@]} > 0)); then - for lockfile in "${EXTRA_LOCKFILES[@]}"; do - args+=(--lockfile="$lockfile") - done -else - args+=(--recursive "$ROOT") -fi - -output_file="$(mktemp)" -trap 'rm -f "$output_file"' EXIT - -set +e -osv-scanner "${args[@]}" >"$output_file" 2>&1 -status=$? -set -e - -cat "$output_file" - -if [[ "$status" -eq 128 ]] && grep -q "No package sources found" "$output_file"; then - echo "PASS: no scannable dependency manifests (OSV skipped)" - exit 0 -fi - -if [[ "$status" -gt 1 ]]; then - echo "osv-scanner failed unexpectedly (exit $status)" >&2 - exit "$status" -fi - -python3 - "$output_file" <<'PY' -import re -import sys - -with open(sys.argv[1], encoding="utf-8") as fh: - text = fh.read() - -if "No issues found" in text: - print("PASS: no OSV findings") - raise SystemExit(0) - -match = re.search( - r"\((\d+) Critical, (\d+) High, (\d+) Medium, (\d+) Low", - text, -) -if not match: - if re.search(r"\b(GHSA-[a-z0-9-]+|GO-\d{4}-\d+|CVE-\d{4}-\d+)\b", text): - print( - "FAIL: OSV findings present but severity summary missing", - file=sys.stderr, - ) - raise SystemExit(1) - print("PASS: no parseable HIGH/CRITICAL OSV findings") - raise SystemExit(0) - -critical = int(match.group(1)) -high = int(match.group(2)) -if critical or high: - print( - f"FAIL: OSV found {critical} Critical and {high} High findings", - file=sys.stderr, - ) - raise SystemExit(1) - -print( - "PASS: OSV gate " - f"(Critical={critical}, High={high}; Medium/Low non-blocking)" -) -PY