From 7df118d4883ec28fc769a3cac07900f96afa1f42 Mon Sep 17 00:00:00 2001 From: devitway Date: Tue, 17 Mar 2026 10:30:15 +0000 Subject: [PATCH] security: harden OpenSSF Scorecard compliance - Pin all GitHub Actions by SHA hash (Pinned-Dependencies) - Add top-level permissions: read-all (Token-Permissions) - Add explicit job-level permissions (least privilege) - Add OpenSSF Scorecard workflow with weekly schedule - Publish scorecard results to scorecard.dev and GitHub Security tab --- .github/workflows/ci.yml | 47 +++++++++++++++------------------ .github/workflows/release.yml | 36 +++++++++++++------------ .github/workflows/scorecard.yml | 35 ++++++++++++++++++++++++ 3 files changed, 76 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/scorecard.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c3fa95a..a91a122 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,18 +6,20 @@ on: pull_request: branches: [main] +permissions: read-all + jobs: test: name: Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - name: Cache cargo - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 - name: Check formatting run: cargo fmt --check @@ -33,18 +35,18 @@ jobs: runs-on: ubuntu-latest permissions: contents: read - security-events: write # for uploading SARIF to GitHub Security tab + security-events: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 with: - fetch-depth: 0 # full history required for gitleaks + fetch-depth: 0 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - name: Cache cargo - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 # ── Secrets ──────────────────────────────────────────────────────────── - name: Gitleaks — scan for hardcoded secrets @@ -58,11 +60,11 @@ jobs: run: cargo install cargo-audit --locked - name: cargo audit — RustSec advisory database - run: cargo audit --ignore RUSTSEC-2025-0119 # known: number_prefix via indicatif + run: cargo audit --ignore RUSTSEC-2025-0119 # ── Licenses, banned crates, supply chain policy ──────────────────────── - name: cargo deny — licenses and banned crates - uses: EmbarkStudios/cargo-deny-action@v2 + uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2 with: command: check arguments: --all-features @@ -70,17 +72,17 @@ jobs: # ── CVE scan of source tree and Cargo.lock ────────────────────────────── - name: Trivy — filesystem scan (Cargo.lock + source) if: always() - uses: aquasecurity/trivy-action@0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 with: scan-type: fs scan-ref: . format: sarif output: trivy-fs.sarif severity: HIGH,CRITICAL - exit-code: 1 # block pipeline on HIGH/CRITICAL vulnerabilities + exit-code: 1 - name: Upload Trivy fs results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 # v4 if: always() with: sarif_file: trivy-fs.sarif @@ -92,18 +94,17 @@ jobs: needs: test steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Install Rust - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable - name: Cache cargo - uses: Swatinem/rust-cache@v2 + uses: Swatinem/rust-cache@42dc69e1aa15d09112580998cf2ef0119e2e91ae # v2 - name: Build NORA run: cargo build --release --package nora-registry - # -- Start NORA -- - name: Start NORA run: | NORA_STORAGE_PATH=/tmp/nora-data ./target/release/nora & @@ -112,7 +113,6 @@ jobs: done curl -sf http://localhost:4000/health | jq . - # -- Docker push/pull -- - name: Configure Docker for insecure registry run: | echo '{"insecure-registries": ["localhost:4000"]}' | sudo tee /etc/docker/daemon.json @@ -133,38 +133,35 @@ jobs: curl -sf http://localhost:4000/v2/_catalog | jq . curl -sf http://localhost:4000/v2/test/alpine/tags/list | jq . - # -- npm (read-only proxy, no publish support yet) -- - name: npm — verify registry endpoint run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/npm/lodash) echo "npm endpoint returned: $STATUS" [ "$STATUS" != "000" ] && echo "npm endpoint OK" || (echo "npm endpoint unreachable" && exit 1) - # -- Maven deploy/download -- - name: Maven — deploy and download artifact run: | echo "test-artifact-content-$(date +%s)" > /tmp/test-artifact.jar CHECKSUM=$(sha256sum /tmp/test-artifact.jar | cut -d' ' -f1) - curl -sf -X PUT --data-binary @/tmp/test-artifact.jar http://localhost:4000/maven2/com/example/test-lib/1.0.0/test-lib-1.0.0.jar - curl -sf -o /tmp/downloaded.jar http://localhost:4000/maven2/com/example/test-lib/1.0.0/test-lib-1.0.0.jar + curl -sf -X PUT --data-binary @/tmp/test-artifact.jar \ + http://localhost:4000/maven2/com/example/test-lib/1.0.0/test-lib-1.0.0.jar + curl -sf -o /tmp/downloaded.jar \ + http://localhost:4000/maven2/com/example/test-lib/1.0.0/test-lib-1.0.0.jar DOWNLOAD_CHECKSUM=$(sha256sum /tmp/downloaded.jar | cut -d' ' -f1) [ "$CHECKSUM" = "$DOWNLOAD_CHECKSUM" ] && echo "Maven deploy/download OK" || (echo "Checksum mismatch!" && exit 1) - # -- PyPI (read-only proxy, no upload support yet) -- - name: PyPI — verify simple index run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/simple/) echo "PyPI simple index returned: $STATUS" [ "$STATUS" = "200" ] && echo "PyPI endpoint OK" || (echo "Expected 200, got $STATUS" && exit 1) - # -- Cargo (read-only proxy, no publish support yet) -- - name: Cargo — verify registry API responds run: | STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:4000/cargo/api/v1/crates/serde) echo "Cargo API returned: $STATUS" [ "$STATUS" != "000" ] && echo "Cargo endpoint OK" || (echo "Cargo endpoint unreachable" && exit 1) - # -- API checks -- - name: API — health, ready, metrics run: | curl -sf http://localhost:4000/health | jq .status diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 54232d1..857bd34 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,6 +4,8 @@ on: push: tags: ['v*'] +permissions: read-all + env: REGISTRY: ghcr.io NORA: localhost:5000 @@ -18,7 +20,7 @@ jobs: packages: write steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set up Rust run: | @@ -32,19 +34,19 @@ jobs: cp target/x86_64-unknown-linux-musl/release/nora ./nora - name: Upload binary artifact - uses: actions/upload-artifact@v7 + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 with: name: nora-binary-${{ github.run_id }} path: ./nora retention-days: 1 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v4 + uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4 with: driver-opts: network=host - name: Log in to GitHub Container Registry - uses: docker/login-action@v4 + uses: docker/login-action@b45d80f862d83dbcd57f89517bcf500b2ab88fb2 # v4 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -53,7 +55,7 @@ jobs: # ── Alpine ─────────────────────────────────────────────────────────────── - name: Extract metadata (alpine) id: meta-alpine - uses: docker/metadata-action@v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.NORA }}/${{ env.IMAGE_NAME }} @@ -64,7 +66,7 @@ jobs: type=raw,value=latest - name: Build and push (alpine) - uses: docker/build-push-action@v7 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile @@ -78,7 +80,7 @@ jobs: # ── RED OS ─────────────────────────────────────────────────────────────── - name: Extract metadata (redos) id: meta-redos - uses: docker/metadata-action@v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.NORA }}/${{ env.IMAGE_NAME }} @@ -90,7 +92,7 @@ jobs: type=raw,value=redos - name: Build and push (redos) - uses: docker/build-push-action@v7 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile.redos @@ -104,7 +106,7 @@ jobs: # ── Astra Linux SE ─────────────────────────────────────────────────────── - name: Extract metadata (astra) id: meta-astra - uses: docker/metadata-action@v6 + uses: docker/metadata-action@030e881283bb7a6894de51c315a6bfe6a94e05cf # v6 with: images: | ${{ env.NORA }}/${{ env.IMAGE_NAME }} @@ -116,7 +118,7 @@ jobs: type=raw,value=astra - name: Build and push (astra) - uses: docker/build-push-action@v7 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7 with: context: . file: Dockerfile.astra @@ -165,7 +167,7 @@ jobs: run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Trivy — image scan (${{ matrix.name }}) - uses: aquasecurity/trivy-action@0.35.0 + uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0 with: scan-type: image image-ref: ${{ env.NORA }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }}${{ matrix.suffix }} @@ -175,7 +177,7 @@ jobs: exit-code: 1 - name: Upload Trivy image results to GitHub Security tab - uses: github/codeql-action/upload-sarif@v4 + uses: github/codeql-action/upload-sarif@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 # v4 if: always() with: sarif_file: trivy-image-${{ matrix.name }}.sarif @@ -190,14 +192,14 @@ jobs: packages: read steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 - name: Set version tag (strip leading v) id: ver run: echo "tag=${GITHUB_REF_NAME#v}" >> $GITHUB_OUTPUT - name: Download binary artifact - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: name: nora-binary-${{ github.run_id }} path: ./artifacts @@ -211,21 +213,21 @@ jobs: cat nora-linux-amd64.sha256 - name: Generate SBOM (SPDX) - uses: anchore/sbom-action@v0 + uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0 with: image: ${{ env.NORA }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }} format: spdx-json output-file: nora-${{ github.ref_name }}.sbom.spdx.json - name: Generate SBOM (CycloneDX) - uses: anchore/sbom-action@v0 + uses: anchore/sbom-action@57aae528053a48a3f6235f2d9461b05fbcb7366d # v0 with: image: ${{ env.NORA }}/${{ env.IMAGE_NAME }}:${{ steps.ver.outputs.tag }} format: cyclonedx-json output-file: nora-${{ github.ref_name }}.sbom.cdx.json - name: Create Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2 with: generate_release_notes: true files: | diff --git a/.github/workflows/scorecard.yml b/.github/workflows/scorecard.yml new file mode 100644 index 0000000..f23cfce --- /dev/null +++ b/.github/workflows/scorecard.yml @@ -0,0 +1,35 @@ +name: OpenSSF Scorecard + +on: + push: + branches: [main] + schedule: + - cron: '0 6 * * 1' # every Monday at 06:00 UTC + +permissions: read-all + +jobs: + analysis: + name: Scorecard analysis + runs-on: ubuntu-latest + permissions: + security-events: write + id-token: write + + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6 + with: + persist-credentials: false + + - name: Run OpenSSF Scorecard + uses: ossf/scorecard-action@99c09fe975337306107572b4fdf4db224cf8e2f2 # v2.4.3 + with: + results_file: results.sarif + results_format: sarif + publish_results: true + + - name: Upload Scorecard results to GitHub Security tab + uses: github/codeql-action/upload-sarif@a60c4df7a135c7317c1e9ddf9b5a9b07a910dda9 # v4 + with: + sarif_file: results.sarif + category: scorecard