diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55f1954..1fd1f45 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,9 @@ jobs: runs-on: ubuntu-latest permissions: contents: write + id-token: write + attestations: write + artifact-metadata: write steps: - name: Checkout uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 @@ -27,9 +30,18 @@ jobs: run: | chmod +x ./gradlew ./gradlew check releaseBundle + - name: Generate release provenance attestation + uses: actions/attest-build-provenance@a2bbfa25375fe432b6a289bc6b6cd05ecd0c4c32 # v4.1.0 + with: + subject-path: build/release/network-chat-*-windows.zip + - name: Generate SBOM attestation + uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0 + with: + subject-path: build/release/network-chat-*-windows.zip + sbom-path: build/release/network-chat-*-sbom.cdx.json - name: Upload release assets if: github.event_name == 'release' env: GH_TOKEN: ${{ github.token }} run: | - gh release upload "${GITHUB_REF_NAME}" build/release/*.zip build/release/checksums.txt build/release/provenance.json --clobber + gh release upload "${GITHUB_REF_NAME}" build/release/*.zip build/release/checksums.txt build/release/provenance.json build/release/*-sbom.cdx.json --clobber diff --git a/README.en.md b/README.en.md index c1daa12..2463572 100644 --- a/README.en.md +++ b/README.en.md @@ -19,7 +19,7 @@ Network Chat is a Java 21 chat application over TCP sockets with: - optional file-backed message history with room replay after server restart. - optional TLS mode, token-based accounts with `USER`/`ADMIN` roles, and the admin `/health` command. -- a Windows release zip with launch scripts, checksums, and provenance metadata. +- a Windows release zip with launch scripts, checksums, CycloneDX SBOM, and provenance metadata. - reproducible Gradle build, tests, CI, and quality gates. ![Swing GUI client](docs/images/gui-client.svg) @@ -84,7 +84,14 @@ To build a no-Gradle release package for end users: ./gradlew releaseBundle ``` -Artifacts are written to `build/release`: the Windows zip, `checksums.txt`, and `provenance.json`. +Artifacts are written to `build/release`: the Windows zip, `checksums.txt`, CycloneDX SBOM, and `provenance.json`. + +Release verification: + +```bash +sha256sum -c checksums.txt +gh attestation verify network-chat-*-windows.zip --repo krotname/JavaNetworkChat +``` ## Architecture and protocol @@ -177,6 +184,7 @@ This repository is organized for maintainability: - Automated quality gates via Checkstyle, Spotless, SpotBugs and JaCoCo. - Security checks via CodeQL and OpenSSF Scorecard. - Dependency and workflow automation via Dependabot. +- Release artifacts: zip, `checksums.txt`, CycloneDX SBOM, provenance metadata, and GitHub attestations. - Explicit contributor and security docs. ## Troubleshooting diff --git a/README.md b/README.md index c6c3bb8..dbb4183 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Network Chat — это Java 21 приложение для сетевого ч - опциональная файловая история сообщений с replay последних сообщений комнаты после рестарта; - опциональный TLS-режим, token-based accounts с ролями `USER`/`ADMIN` и admin-команда `/health`; -- Windows release zip с launch scripts, checksums и provenance metadata; +- Windows release zip с launch scripts, checksums, CycloneDX SBOM и provenance metadata; - воспроизводимая Gradle-сборка, тесты, CI и проверки качества. ![Swing GUI client](docs/images/gui-client.svg) @@ -82,7 +82,14 @@ $env:NETWORK_CHAT_TRUSTSTORE_PASSWORD="changeit" ./gradlew releaseBundle ``` -Артефакты появятся в `build/release`: Windows zip, `checksums.txt` и `provenance.json`. +Артефакты появятся в `build/release`: Windows zip, `checksums.txt`, CycloneDX SBOM и `provenance.json`. + +Проверка релиза: + +```bash +sha256sum -c checksums.txt +gh attestation verify network-chat-*-windows.zip --repo krotname/JavaNetworkChat +``` ## Архитектура и протокол @@ -160,6 +167,7 @@ Actions Summary для Linux job. - Авто-проверки: Checkstyle, Spotless, SpotBugs, JaCoCo. - Security проверки: CodeQL и OpenSSF Scorecard. - Dependabot с группировкой обновлений зависимостей и Actions. +- Релизные артефакты: zip, `checksums.txt`, CycloneDX SBOM, provenance metadata и GitHub attestations. - Явно оформленные файлы `CONTRIBUTING.md` и `SECURITY.md`. ## Roadmap diff --git a/SECURITY.md b/SECURITY.md index dfdc014..810a702 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -34,7 +34,7 @@ Protected assets: - chat message contents in transit when TLS is enabled, - account tokens stored only as salted SHA-256 hashes in the optional accounts file, - server availability under configured client limits, -- release artifacts and their checksums/provenance metadata. +- release artifacts, CycloneDX SBOM, checksums, provenance metadata, and GitHub attestations. Trust boundaries: @@ -59,4 +59,4 @@ Operational recommendations: - Enable TLS outside localhost/trusted lab networks. - Keep `accounts.csv` readable only by the server operator. - Rotate tokens by replacing account-file rows and restarting the server. -- Publish release zip files together with `checksums.txt` and `provenance.json`. +- Publish release zip files together with `checksums.txt`, CycloneDX SBOM, `provenance.json`, and GitHub attestations. diff --git a/build.gradle.kts b/build.gradle.kts index 1321228..e8d76e3 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,7 @@ import java.security.MessageDigest import java.time.Instant +import java.util.UUID +import groovy.json.JsonOutput plugins { java @@ -267,6 +269,7 @@ val packageWindowsZip = tasks.register("packageWindowsZip") { } val provenanceFile = layout.buildDirectory.file("release/provenance.json") +val sbomFile = layout.buildDirectory.file("release/$releasePackageName-sbom.cdx.json") tasks.register("releaseProvenance") { group = "distribution" @@ -306,15 +309,16 @@ tasks.register("releaseProvenance") { tasks.register("releaseChecksums") { group = "distribution" description = "Write SHA-256 checksums for release artifacts" - dependsOn(packageWindowsZip, tasks.named("releaseProvenance")) + dependsOn(packageWindowsZip, tasks.named("releaseProvenance"), "releaseSbom") val checksumsFile = layout.buildDirectory.file("release/checksums.txt") - inputs.files(packageWindowsZip.flatMap { it.archiveFile }, provenanceFile) + inputs.files(packageWindowsZip.flatMap { it.archiveFile }, provenanceFile, sbomFile) outputs.file(checksumsFile) doLast { val artifacts = listOf( packageWindowsZip.get().archiveFile.get().asFile, provenanceFile.get().asFile, + sbomFile.get().asFile, ) checksumsFile.get().asFile.writeText( artifacts.joinToString(System.lineSeparator()) { @@ -324,10 +328,52 @@ tasks.register("releaseChecksums") { } } +tasks.register("releaseSbom") { + group = "distribution" + description = "Write a CycloneDX SBOM for the release runtime classpath" + outputs.file(sbomFile) + doLast { + val components = + configurations.runtimeClasspath.get().resolvedConfiguration.resolvedArtifacts + .sortedWith(compareBy({ it.moduleVersion.id.group }, { it.name }, { it.moduleVersion.id.version })) + .map { + val id = it.moduleVersion.id + mapOf( + "type" to "library", + "bom-ref" to "pkg:maven/${id.group}/${id.name}@${id.version}", + "group" to id.group, + "name" to id.name, + "version" to id.version, + "purl" to "pkg:maven/${id.group}/${id.name}@${id.version}", + ) + } + val bom = + mapOf( + "bomFormat" to "CycloneDX", + "specVersion" to "1.6", + "serialNumber" to "urn:uuid:${UUID.nameUUIDFromBytes("${project.name}:${project.version}".toByteArray())}", + "version" to 1, + "metadata" to + mapOf( + "timestamp" to Instant.now().toString(), + "component" to + mapOf( + "type" to "application", + "name" to project.name, + "version" to project.version.toString(), + "purl" to "pkg:maven/${project.group}/${project.name}@${project.version}", + ), + ), + "components" to components, + ) + sbomFile.get().asFile.writeText(JsonOutput.prettyPrint(JsonOutput.toJson(bom))) + } +} + tasks.register("releaseBundle") { group = "distribution" - description = "Build release zip, checksums, and provenance metadata" - dependsOn(packageWindowsZip, tasks.named("releaseProvenance"), tasks.named("releaseChecksums")) + description = "Build release zip, checksums, SBOM, and provenance metadata" + dependsOn(packageWindowsZip, tasks.named("releaseProvenance"), tasks.named("releaseSbom"), tasks.named("releaseChecksums")) } fun sha256(file: File): String {