From 6ad710ff3208ea32e365b4fb0cb0829ba56e916a Mon Sep 17 00:00:00 2001 From: devitway Date: Mon, 23 Feb 2026 11:37:27 +0000 Subject: [PATCH] ci: add security scanning and SBOM to release pipeline - ci.yml: add security job (gitleaks, cargo-audit, cargo-deny, trivy fs) - release.yml: restructure into build-binary + build-docker matrix + release - build binary once on self-hosted, reuse across all Docker builds - trivy image scan per matrix variant, results to GitHub Security tab - SBOM generation in SPDX and CycloneDX formats attached to release - deny.toml: cargo-deny policy (allowed licenses, banned openssl, crates.io only) - Dockerfile: remove Rust build stage, use pre-built binary - Dockerfile.astra, Dockerfile.redos: FROM scratch for Russian certified OS support --- .github/workflows/ci.yml | 56 ++++++++++++++++++++++++ .github/workflows/release.yml | 80 ++++++++++++++++++++++++++++++++++- Dockerfile | 55 ++---------------------- Dockerfile.astra | 53 ++++------------------- Dockerfile.redos | 53 ++++------------------- deny.toml | 41 ++++++++++++++++++ 6 files changed, 197 insertions(+), 141 deletions(-) create mode 100644 deny.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c22569b..4ca408d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,3 +27,59 @@ jobs: - name: Run tests run: cargo test --package nora-registry + + security: + name: Security + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write # for uploading SARIF to GitHub Security tab + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history required for gitleaks + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo + uses: Swatinem/rust-cache@v2 + + # ── Secrets ──────────────────────────────────────────────────────────── + - name: Gitleaks — scan for hardcoded secrets + uses: gitleaks/gitleaks-action@v2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + # ── CVE in Rust dependencies ──────────────────────────────────────────── + - name: Install cargo-audit + run: cargo install cargo-audit --locked + + - name: cargo audit — RustSec advisory database + run: cargo audit + + # ── Licenses, banned crates, supply chain policy ──────────────────────── + - name: cargo deny — licenses and banned crates + uses: EmbarkStudios/cargo-deny-action@v2 + with: + command: check + arguments: --all-features + + # ── CVE scan of source tree and Cargo.lock ────────────────────────────── + - name: Trivy — filesystem scan (Cargo.lock + source) + uses: aquasecurity/trivy-action@master + with: + scan-type: fs + scan-ref: . + format: sarif + output: trivy-fs.sarif + severity: HIGH,CRITICAL + exit-code: 0 # warn only; change to 1 to block the pipeline + + - name: Upload Trivy fs results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-fs.sarif + category: trivy-fs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4b3bf16..aee6efe 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -9,12 +9,36 @@ env: IMAGE_NAME: ${{ github.repository }} jobs: - build: + build-binary: + name: Build Binary + runs-on: self-hosted + + steps: + - uses: actions/checkout@v4 + + - name: Set up Rust + run: echo "/home/ai-user/.rustup/toolchains/stable-x86_64-unknown-linux-gnu/bin" >> $GITHUB_PATH + + - name: Build release binary + run: | + RUSTUP_HOME=/home/ai-user/.rustup cargo build --release --package nora-registry + cp target/release/nora ./nora + + - name: Upload binary + uses: actions/upload-artifact@v4 + with: + name: nora-binary + path: nora + retention-days: 1 + + build-docker: name: Build & Push (${{ matrix.name }}) runs-on: self-hosted + needs: build-binary permissions: contents: read packages: write + security-events: write # for uploading SARIF to GitHub Security tab strategy: fail-fast: false @@ -33,6 +57,14 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Download binary + uses: actions/download-artifact@v4 + with: + name: nora-binary + + - name: Make binary executable + run: chmod +x nora + - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -68,20 +100,64 @@ jobs: cache-from: type=gha,scope=${{ matrix.name }} cache-to: type=gha,mode=max,scope=${{ matrix.name }} + # ── CVE scan of the pushed image ──────────────────────────────────────── + - name: Trivy — image scan + uses: aquasecurity/trivy-action@master + with: + scan-type: image + image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}${{ matrix.suffix }} + format: sarif + output: trivy-image-${{ matrix.name }}.sarif + severity: HIGH,CRITICAL + exit-code: 0 # warn only; change to 1 to block on vulnerabilities + + - name: Upload Trivy image results to GitHub Security tab + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: trivy-image-${{ matrix.name }}.sarif + category: trivy-image-${{ matrix.name }} + release: name: GitHub Release runs-on: ubuntu-latest - needs: build + needs: build-docker permissions: contents: write + packages: read # to pull image for SBOM generation steps: - uses: actions/checkout@v4 + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # ── SBOM — Software Bill of Materials ─────────────────────────────────── + - name: Generate SBOM (SPDX) + uses: anchore/sbom-action@v0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + format: spdx-json + output-file: nora-${{ github.ref_name }}.sbom.spdx.json + + - name: Generate SBOM (CycloneDX) + uses: anchore/sbom-action@v0 + with: + image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }} + format: cyclonedx-json + output-file: nora-${{ github.ref_name }}.sbom.cdx.json + - name: Create Release uses: softprops/action-gh-release@v1 with: generate_release_notes: true + files: | + nora-${{ github.ref_name }}.sbom.spdx.json + nora-${{ github.ref_name }}.sbom.cdx.json body: | ## Docker diff --git a/Dockerfile b/Dockerfile index 4f6470f..282d2dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,58 +1,11 @@ # syntax=docker/dockerfile:1.4 - -# Build stage -FROM rust:1.83-alpine AS builder - -RUN apk add --no-cache musl-dev curl - -WORKDIR /app - -# Copy manifests -COPY Cargo.toml Cargo.lock ./ -COPY nora-registry/Cargo.toml nora-registry/ -COPY nora-storage/Cargo.toml nora-storage/ -COPY nora-cli/Cargo.toml nora-cli/ - -# Create dummy sources for dependency caching -RUN mkdir -p nora-registry/src nora-storage/src nora-cli/src && \ - echo "fn main() {}" > nora-registry/src/main.rs && \ - echo "fn main() {}" > nora-storage/src/main.rs && \ - echo "fn main() {}" > nora-cli/src/main.rs - -# Build dependencies only (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - cargo build --release --package nora-registry && \ - rm -rf nora-registry/src nora-storage/src nora-cli/src - -# Copy real sources -COPY nora-registry/src nora-registry/src -COPY nora-storage/src nora-storage/src -COPY nora-cli/src nora-cli/src - -# Build release binary (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - touch nora-registry/src/main.rs && \ - cargo build --release --package nora-registry && \ - cp /app/target/release/nora /usr/local/bin/nora - -# Runtime stage +# Binary is pre-built by CI (cargo build --release) and passed via context FROM alpine:3.20 -RUN apk add --no-cache ca-certificates +RUN apk add --no-cache ca-certificates && mkdir -p /data -WORKDIR /app +COPY nora /usr/local/bin/nora -# Copy binary -COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora - -# Create data directory -RUN mkdir -p /data - -# Default environment ENV RUST_LOG=info ENV NORA_HOST=0.0.0.0 ENV NORA_PORT=4000 @@ -64,5 +17,5 @@ EXPOSE 4000 VOLUME ["/data"] -ENTRYPOINT ["nora"] +ENTRYPOINT ["/usr/local/bin/nora"] CMD ["serve"] diff --git a/Dockerfile.astra b/Dockerfile.astra index 40f61a1..bfd7627 100644 --- a/Dockerfile.astra +++ b/Dockerfile.astra @@ -1,52 +1,17 @@ # syntax=docker/dockerfile:1.4 +# Binary is pre-built by CI (cargo build --release) and passed via context +# Runtime: scratch — compatible with Astra Linux SE (FSTEC certified) +# To switch to official base: replace FROM scratch with +# FROM registry.astralinux.ru/library/alse:latest +# RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && rm -rf /var/lib/apt/lists/* -# Build stage — static binary via musl (runs on any Linux) -FROM rust:1.83-alpine AS builder +FROM alpine:3.20 AS certs +RUN apk add --no-cache ca-certificates -RUN apk add --no-cache musl-dev curl - -WORKDIR /app - -# Copy manifests -COPY Cargo.toml Cargo.lock ./ -COPY nora-registry/Cargo.toml nora-registry/ -COPY nora-storage/Cargo.toml nora-storage/ -COPY nora-cli/Cargo.toml nora-cli/ - -# Create dummy sources for dependency caching -RUN mkdir -p nora-registry/src nora-storage/src nora-cli/src && \ - echo "fn main() {}" > nora-registry/src/main.rs && \ - echo "fn main() {}" > nora-storage/src/main.rs && \ - echo "fn main() {}" > nora-cli/src/main.rs - -# Build dependencies only (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - cargo build --release --package nora-registry && \ - rm -rf nora-registry/src nora-storage/src nora-cli/src - -# Copy real sources -COPY nora-registry/src nora-registry/src -COPY nora-storage/src nora-storage/src -COPY nora-cli/src nora-cli/src - -# Build release binary (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - touch nora-registry/src/main.rs && \ - cargo build --release --package nora-registry && \ - cp /app/target/release/nora /usr/local/bin/nora - -# Runtime stage — scratch (compatible with Astra Linux SE, no foreign OS components) -# Switch FROM to registry.astralinux.ru/library/alse once registry access is configured FROM scratch -# CA certificates for TLS -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt - -COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY nora /usr/local/bin/nora ENV RUST_LOG=info ENV NORA_HOST=0.0.0.0 diff --git a/Dockerfile.redos b/Dockerfile.redos index 1b276be..4010bf9 100644 --- a/Dockerfile.redos +++ b/Dockerfile.redos @@ -1,52 +1,17 @@ # syntax=docker/dockerfile:1.4 +# Binary is pre-built by CI (cargo build --release) and passed via context +# Runtime: scratch — compatible with RED OS (FSTEC certified) +# To switch to official base: replace FROM scratch with +# FROM registry.red-soft.ru/redos/redos:8 +# RUN dnf install -y ca-certificates && dnf clean all -# Build stage — static binary via musl (runs on any Linux) -FROM rust:1.83-alpine AS builder +FROM alpine:3.20 AS certs +RUN apk add --no-cache ca-certificates -RUN apk add --no-cache musl-dev curl - -WORKDIR /app - -# Copy manifests -COPY Cargo.toml Cargo.lock ./ -COPY nora-registry/Cargo.toml nora-registry/ -COPY nora-storage/Cargo.toml nora-storage/ -COPY nora-cli/Cargo.toml nora-cli/ - -# Create dummy sources for dependency caching -RUN mkdir -p nora-registry/src nora-storage/src nora-cli/src && \ - echo "fn main() {}" > nora-registry/src/main.rs && \ - echo "fn main() {}" > nora-storage/src/main.rs && \ - echo "fn main() {}" > nora-cli/src/main.rs - -# Build dependencies only (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - cargo build --release --package nora-registry && \ - rm -rf nora-registry/src nora-storage/src nora-cli/src - -# Copy real sources -COPY nora-registry/src nora-registry/src -COPY nora-storage/src nora-storage/src -COPY nora-cli/src nora-cli/src - -# Build release binary (with cache) -RUN --mount=type=cache,target=/usr/local/cargo/registry \ - --mount=type=cache,target=/usr/local/cargo/git \ - --mount=type=cache,target=/app/target \ - touch nora-registry/src/main.rs && \ - cargo build --release --package nora-registry && \ - cp /app/target/release/nora /usr/local/bin/nora - -# Runtime stage — scratch (compatible with RED OS, no foreign OS components) -# Switch FROM to registry.red-soft.ru/redos once registry access is configured FROM scratch -# CA certificates for TLS -COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt - -COPY --from=builder /usr/local/bin/nora /usr/local/bin/nora +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt +COPY nora /usr/local/bin/nora ENV RUST_LOG=info ENV NORA_HOST=0.0.0.0 diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..117dca3 --- /dev/null +++ b/deny.toml @@ -0,0 +1,41 @@ +# cargo-deny configuration +# https://embarkstudios.github.io/cargo-deny/ + +[advisories] +# Vulnerability database (RustSec) +db-urls = ["https://github.com/rustsec/advisory-db"] +ignore = [] + +[licenses] +# Allowed open-source licenses +allow = [ + "MIT", + "Apache-2.0", + "Apache-2.0 WITH LLVM-exception", + "BSD-2-Clause", + "BSD-3-Clause", + "ISC", + "Unicode-DFS-2016", + "Unicode-3.0", + "CC0-1.0", + "OpenSSL", + "Zlib", + "MPL-2.0", # Mozilla Public License — ok for binary linking +] +copyleft = "warn" # GPL etc — warn, don't block +unlicensed = "deny" + +[bans] +multiple-versions = "warn" +deny = [ + # Prefer rustls over openssl for static builds and supply chain cleanliness + { name = "openssl-sys" }, + { name = "openssl" }, +] +skip = [] + +[sources] +unknown-registry = "warn" +unknown-git = "warn" +# Allow only the official crates.io index +allow-registry = ["https://github.com/rust-lang/crates.io-index"]