Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
12 changes: 10 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand Down
12 changes: 10 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
```

## Архитектура и протокол

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand All @@ -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.
54 changes: 50 additions & 4 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import java.security.MessageDigest
import java.time.Instant
import java.util.UUID
import groovy.json.JsonOutput

plugins {
java
Expand Down Expand Up @@ -267,6 +269,7 @@ val packageWindowsZip = tasks.register<Zip>("packageWindowsZip") {
}

val provenanceFile = layout.buildDirectory.file("release/provenance.json")
val sbomFile = layout.buildDirectory.file("release/$releasePackageName-sbom.cdx.json")

tasks.register("releaseProvenance") {
group = "distribution"
Expand Down Expand Up @@ -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()) {
Expand All @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Declare SBOM task inputs

When releaseBundle is rerun in the same checkout after a runtime dependency changes without changing project.version, Gradle can treat releaseSbom as up-to-date because this task declares only its output and no inputs. The zip and provenance/checksum tasks may rebuild, but the old *-sbom.cdx.json remains and gets checksummed/uploaded as the release SBOM, so the published SBOM can describe the previous dependency set; declare the runtime classpath and project metadata as inputs for this task.

Useful? React with 👍 / 👎.

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 {
Expand Down
Loading