feat(release): complete milestone 10 hardening

This commit is contained in:
2026-08-09 18:56:22 +00:00
parent cd654144a0
commit 878af75900
15 changed files with 497 additions and 12 deletions
+8
View File
@@ -0,0 +1,8 @@
.git
.cache
dist
data
secrets
*.db
*.db-shm
*.db-wal
+27
View File
@@ -0,0 +1,27 @@
# syntax=docker/dockerfile:1@sha256:87999aa3d42bdc6bea60565083ee17e86d1f3339802f543c0d03998580f9cb89
FROM golang:1.25-bookworm@sha256:908f8ff2ec296df2f349563072c7925775cd28b50361a52ed834a8a37399b9bf AS build
WORKDIR /src
COPY go.mod go.sum ./
RUN --mount=type=cache,target=/go/pkg/mod go mod download
COPY . .
ARG TARGETOS=linux
ARG TARGETARCH
ARG VERSION=dev
ARG COMMIT=unknown
RUN --mount=type=cache,target=/go/pkg/mod --mount=type=cache,target=/root/.cache/go-build \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama ./cmd/dogama && \
CGO_ENABLED=0 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$VERSION -X main.commit=$COMMIT" -o /out/dogama-agent ./cmd/dogama-agent
FROM gcr.io/distroless/static-debian12:nonroot@sha256:f5b485ea962d9bd1186b2f6b3a061191539b905b82ec395de78cbfae51f20e35 AS dogama
WORKDIR /var/lib/dogama
COPY --from=build /out/dogama /usr/local/bin/dogama
EXPOSE 8080
ENTRYPOINT ["/usr/local/bin/dogama"]
FROM gcr.io/distroless/static-debian12:latest@sha256:a9fcaedd4c9b59e12dd65d954f0b5044f19b0647a8a3712e77205df9e7b102cd AS dogama-agent
WORKDIR /var/lib/dogama-agent
COPY --from=build /out/dogama-agent /usr/local/bin/dogama-agent
EXPOSE 8081
ENTRYPOINT ["/usr/local/bin/dogama-agent"]
+17
View File
@@ -0,0 +1,17 @@
.PHONY: test e2e images release
VERSION ?= dev
COMMIT ?= $(shell git rev-parse --short=12 HEAD)
test:
go test ./...
e2e:
go test ./tests/e2e -v
images:
docker build --target dogama --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) -t dogama:$(VERSION) .
docker build --target dogama-agent --build-arg VERSION=$(VERSION) --build-arg COMMIT=$(COMMIT) -t dogama-agent:$(VERSION) .
release:
./tools/release.sh $(VERSION)
+4 -2
View File
@@ -4,7 +4,7 @@
DoGaMa is a lightweight, self-hosted manager for private game servers running as Docker containers. It is designed for families and small groups of friends, not for commercial hosting or general Docker administration. DoGaMa is a lightweight, self-hosted manager for private game servers running as Docker containers. It is designed for families and small groups of friends, not for commercial hosting or general Docker administration.
This repository currently contains the normative product and engineering specification. Implementation must follow the documents and machine-readable contracts linked below. This repository contains the deployable DoGaMa V1 implementation and its normative product and engineering contracts.
## Product invariants ## Product invariants
@@ -41,6 +41,7 @@ This repository currently contains the normative product and engineering specifi
- [Backups, import, restore and export](docs/operations/backups-import-export.md) - [Backups, import, restore and export](docs/operations/backups-import-export.md)
- [Resources, ports, storage, mods and updates](docs/operations/instance-operations.md) - [Resources, ports, storage, mods and updates](docs/operations/instance-operations.md)
- [Notifications and audit](docs/operations/notifications-and-audit.md) - [Notifications and audit](docs/operations/notifications-and-audit.md)
- [Deployment and release](docs/operations/deployment-and-release.md)
- [Security and threat model](docs/security/security-and-threat-model.md) - [Security and threat model](docs/security/security-and-threat-model.md)
- [Administration and manager interfaces](docs/ux/interfaces.md) - [Administration and manager interfaces](docs/ux/interfaces.md)
@@ -48,6 +49,7 @@ This repository currently contains the normative product and engineering specifi
- [Current operational project state](docs/PROJECT-STATE.md) - [Current operational project state](docs/PROJECT-STATE.md)
- [Development conventions](docs/contributing/development.md) - [Development conventions](docs/contributing/development.md)
- [Contributor testing](docs/contributing/testing.md)
- [AI and Codex contributor guide](docs/contributing/ai-codex-guide.md) - [AI and Codex contributor guide](docs/contributing/ai-codex-guide.md)
- [Template schema](specs/template.schema.json) - [Template schema](specs/template.schema.json)
- [Module manifest schema](specs/module-manifest.schema.json) - [Module manifest schema](specs/module-manifest.schema.json)
@@ -75,7 +77,7 @@ Only the main application's HTTP port is published. The agent and game-managemen
## Status ## Status
The first seven roadmap foundations are implemented: application/authentication, the restricted agent boundary, the validated catalog, registered instance lifecycle, per-instance authorization, recoverable game-data backups, and the WebAssembly integration runtime. DoGaMa creates atomic `tar.zst` archives with manifests and SHA-256 metadata, selectively retains scheduled backups, supports five-field cron policies with IANA timezones, stages hostile imports under strict limits, and restores through validated staging with a default `pre_restore` safety backup. The module runtime executes typed, capability-checked adapters with bounded resources and instance-pinned networking; the bundled Palworld REST reference adapter is compiled reproducibly and covered by sandbox integration tests. Updates remain later roadmap work. The V1 roadmap is implemented. See the current operational baseline and known limitations in [PROJECT-STATE.md](docs/PROJECT-STATE.md), and use the deployment guide for production installation and release verification.
## Validate the specification ## Validate the specification
+6 -1
View File
@@ -16,6 +16,11 @@ import (
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog" "git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/catalog"
) )
var (
version = "dev"
commit = "unknown"
)
func main() { func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := run(logger); err != nil { if err := run(logger); err != nil {
@@ -67,7 +72,7 @@ func run(logger *slog.Logger) error {
defer stop() defer stop()
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
logger.Info("agent listening", "event", "agent.started", "address", config.ListenAddress, "allowed_root_count", paths.RootCount()) logger.Info("agent listening", "event", "agent.started", "address", config.ListenAddress, "allowed_root_count", paths.RootCount(), "version", version, "commit", commit)
errCh <- server.ListenAndServe() errCh <- server.ListenAndServe()
}() }()
select { select {
+7 -1
View File
@@ -26,6 +26,11 @@ import (
"git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/web" "git.zaynet.fr/DoGaMa/DoGaMa-serv/internal/web"
) )
var (
version = "dev"
commit = "unknown"
)
func main() { func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil)) logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
if err := run(logger); err != nil { if err := run(logger); err != nil {
@@ -127,10 +132,11 @@ func run(logger *slog.Logger) error {
ReadTimeout: 15 * time.Second, ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Minute, WriteTimeout: 15 * time.Minute,
IdleTimeout: 60 * time.Second, IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 16 << 10,
} }
errCh := make(chan error, 1) errCh := make(chan error, 1)
go func() { go func() {
logger.Info("application listening", "event", "application.started", "address", listenAddress) logger.Info("application listening", "event", "application.started", "address", listenAddress, "version", version, "commit", commit)
errCh <- server.ListenAndServe() errCh <- server.ListenAndServe()
}() }()
select { select {
+9
View File
@@ -2,6 +2,7 @@ services:
dogama: dogama:
image: ghcr.io/dogama/dogama:${DOGAMA_VERSION:-latest} image: ghcr.io/dogama/dogama:${DOGAMA_VERSION:-latest}
restart: unless-stopped restart: unless-stopped
read_only: true
ports: ports:
- "${DOGAMA_HTTP_PORT:-8080}:8080" - "${DOGAMA_HTTP_PORT:-8080}:8080"
environment: environment:
@@ -19,6 +20,12 @@ services:
- ./data:/var/lib/dogama - ./data:/var/lib/dogama
- ${DOGAMA_SERVERS_ROOT:-/srv/game-servers}:/srv/game-servers - ${DOGAMA_SERVERS_ROOT:-/srv/game-servers}:/srv/game-servers
- ${DOGAMA_BACKUPS_ROOT:-/srv/game-backups}:/srv/game-backups - ${DOGAMA_BACKUPS_ROOT:-/srv/game-backups}:/srv/game-backups
tmpfs:
- /tmp:rw,noexec,nosuid,nodev,size=32m
security_opt:
- no-new-privileges:true
cap_drop:
- ALL
networks: networks:
- frontend - frontend
- control - control
@@ -50,6 +57,8 @@ services:
- /tmp:rw,noexec,nosuid,nodev,size=16m - /tmp:rw,noexec,nosuid,nodev,size=16m
security_opt: security_opt:
- no-new-privileges:true - no-new-privileges:true
cap_drop:
- ALL
networks: networks:
- control - control
- games - games
+9 -6
View File
@@ -4,9 +4,9 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Baseline ## Baseline
- Current reference: pre-milestone-10 UI redesign branch from merged milestone 9 baseline `d68d97d`. - Current reference: milestone 10 working branch from the UI-redesign baseline `cd65414`.
- Released SQLite migrations: `0001` through `0009`; never rewrite them. - Released SQLite migrations: `0001` through `0009`; never rewrite them.
- Roadmap milestones 1-9 are implemented. - Roadmap milestones 1-10 are implemented; this is the V1 feature baseline.
## Architecture ## Architecture
@@ -33,6 +33,10 @@ Read this compact operational baseline before starting a milestone. Open detaile
- Compact allow-listed audit events for authentication and significant mutations, administrator filtering, bounded manual purge, daily retention and maximum-count enforcement. - Compact allow-listed audit events for authentication and significant mutations, administrator filtering, bounded manual purge, daily retention and maximum-count enforcement.
- Responsive server-rendered application shell with the official square DoGaMa logo, synthwave-derived design tokens, aligned permission-aware navigation, searchable real instance cards and state summaries above the server grid. - Responsive server-rendered application shell with the official square DoGaMa logo, synthwave-derived design tokens, aligned permission-aware navigation, searchable real instance cards and state summaries above the server grid.
- Dedicated administrator Audit and Settings pages; notification channels, audit retention/purge and game-container labels retain their existing backend contracts outside the Dashboard. - Dedicated administrator Audit and Settings pages; notification channels, audit retention/purge and game-container labels retain their existing backend contracts outside the Dashboard.
- Restrictive browser headers and bounded public HTTP headers.
- Hardened read-only Compose services, capability dropping, private agent networking and distinct minimal OCI image targets.
- Linux black-box bootstrap/authentication E2E coverage plus a documented disposable-Docker V1 release verification matrix.
- Deterministic Linux `amd64`/`arm64` archives with embedded build identity, SPDX module SBOM and SHA-256 checksums.
## Durable decisions ## Durable decisions
@@ -56,7 +60,6 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Known limitations and debt ## Known limitations and debt
- Release hardening remains roadmap work.
- Scheduled backup outcomes and repeated authentication blocks are audited/logged, but broader scheduler-origin notification coverage remains intentionally limited to events emitted by implemented workflows. - Scheduled backup outcomes and repeated authentication blocks are audited/logged, but broader scheduler-origin notification coverage remains intentionally limited to events emitted by implemented workflows.
- Instance detail, catalog and backup management remain API-first; their sidebar entries are deliberately disabled until corresponding web pages exist, so navigation does not imply unavailable routes. - Instance detail, catalog and backup management remain API-first; their sidebar entries are deliberately disabled until corresponding web pages exist, so navigation does not imply unavailable routes.
- Linux is the deployment target. Native Windows execution of the full Go suite is blocked by Unix `Statfs` code; use Linux/WSL/CI for complete execution. - Linux is the deployment target. Native Windows execution of the full Go suite is blocked by Unix `Statfs` code; use Linux/WSL/CI for complete execution.
@@ -64,12 +67,12 @@ Read this compact operational baseline before starting a milestone. Open detaile
## Validation and CI ## Validation and CI
- No repository-hosted Gitea/GitHub workflow files are currently present. - No repository-hosted Gitea/GitHub workflow files are present; release validation is host-agnostic and documented through `Makefile`, `AGENTS.md` and the contributor testing guide.
- Normal completion gate for Go changes is the validation set in `AGENTS.md` on Linux. - Normal completion gate for Go changes is the validation set in `AGENTS.md` on Linux.
- Specification validation is `python tools/validate_spec.py` with `tools/requirements-validation.txt` available. - Specification validation is `python tools/validate_spec.py` with `tools/requirements-validation.txt` available.
- Start with package/file-specific tests, then run global tests, build, race detection, vet, static analysis and schema validation as applicable. - Start with package/file-specific tests, then run global tests, build, race detection, vet, static analysis and schema validation as applicable.
## Next known work ## Next known work
- Roadmap milestone 10: security hardening, end-to-end tests, contributor documentation and release packaging. - No V1 milestone remains. Do not begin V1.x or V2 work without an explicit accepted scope.
- Update this file at the end of every merged milestone or durable architectural change; keep it compact and remove stale statements. - Update this file after every merged milestone or durable architectural change; keep it compact and remove stale statements.
+35
View File
@@ -0,0 +1,35 @@
# Contributor testing
Run the smallest affected package first, then the complete gate from
`AGENTS.md`. `make e2e` runs the Linux black-box bootstrap test against the real
`dogama` binary and a temporary SQLite database; it verifies first-run closure,
CSRF denial, secure cookies, security headers, authentication and protected
catalog access.
The following V1 critical paths are intentionally split between deterministic
integration tests and disposable-Docker release verification:
| Boundary or workflow | Automated coverage |
|---|---|
| Bootstrap, login, logout, CSRF, session rotation and throttling | `internal/web` and `tests/e2e` |
| Admin/user/manager membership and explicit-deny behavior | `internal/web` and `internal/authorization` |
| Agent authentication, replay, request bounds and unrelated-container denial | `internal/agent` |
| Path, symlink, plan, image, port and label restrictions | `internal/agent` |
| Archive traversal, links, extraction limits, backup integrity and restore safety | `internal/importexport` and `internal/backup` |
| WASM capability, network, fuel, memory, response and concurrency bounds | `internal/module` |
| Digest update success, failed readiness and rollback | `internal/instance` and `internal/web` |
| Empty and prior-schema migrations | `internal/persistence/sqlite` |
Before publishing a release, additionally use an isolated Docker daemon with
disposable host roots. Run `docker compose config --quiet`, build both image
targets, confirm that the main container has no socket and the agent has no
published port, then exercise Palworld draft/install/start/stop, backup/restore,
an intentionally failing digest update, and container-only deletion. Confirm
that unrelated containers cannot be inspected or mutated and that player and
backup roots remain after deletion and interruption. Never point this test at a
host containing valuable containers or player data.
Tests must use generated secrets and fixtures. Do not place real credentials,
host paths, saves, database copies or registry tokens in logs or commits. A
failure report should name the operation and stable error code without copying
secret-bearing request bodies.
+77
View File
@@ -0,0 +1,77 @@
# Deployment and release
## Production prerequisites
DoGaMa V1 targets one Linux Docker host with the Compose plugin. Put the public
application behind a trusted TLS reverse proxy; never publish the agent port.
Create the server and backup roots on the host and restrict them to the
administrator responsible for DoGaMa. Back up `data/dogama.db`, the server
roots, and the backup roots using host-level tooling.
Create secrets before the first start. Both files must be readable only by the
deployment administrator; the master key is exactly 32 bytes and the agent
token is at least 32 bytes.
```sh
install -d -m 0700 secrets
install -d -m 0750 -o 65532 -g 65532 data
umask 077
head -c 32 /dev/urandom > secrets/master_key
head -c 32 /dev/urandom > secrets/agent_token
docker compose config --quiet
DOGAMA_VERSION=v1.0.0 docker compose up -d
```
Pin `DOGAMA_VERSION` to an immutable released version in production. The main
application runs as a non-root user with a read-only root filesystem, no Linux
capabilities and no Docker socket. The root-running restricted agent is isolated
on the private control network, has a read-only root filesystem and no added
capabilities; only it receives the Docker socket. The game and backup bind roots
remain writable because lifecycle and recovery workflows require them.
TLS termination must retain DoGaMa's CSP, HSTS, frame, MIME and referrer
headers. Do not make forwarded client addresses authoritative for login rate
limiting. After startup, verify that only the configured application port is
published and that normal use redirects to `/setup` until the first
administrator is created.
The application image uses numeric UID/GID `65532:65532`. Grant that identity
write access to the configured server and backup roots (with ACLs or matching
ownership) while keeping access unavailable to unrelated host users.
## Release procedure
From a clean, signed-off release commit, run the complete validation gate in
`AGENTS.md`, then create deterministic Linux archives:
```sh
make release VERSION=v1.0.0
(cd dist/dogama-v1.0.0 && sha256sum -c SHA256SUMS)
```
The release command refuses to overwrite an existing release directory. It
produces static `amd64` and `arm64` archives, embedded Go build information, an
SPDX 2.3 module SBOM and SHA-256 checksums. `SOURCE_DATE_EPOCH` defaults to the
release commit timestamp and may be supplied explicitly for reproduction.
Build the two OCI images from the same commit and version:
```sh
make images VERSION=v1.0.0
```
The Dockerfile has distinct `dogama` and `dogama-agent` targets. Publish both
images under the same immutable version and record their registry digests in
the release notes. A release is complete only after a fresh-host Compose smoke
test, bootstrap, Palworld draft/install against a disposable Docker daemon,
backup/restore, failed-update rollback, and the security-denial checks described
in the contributor testing guide.
## Upgrade and rollback
Stop the application, take a filesystem-consistent copy of the SQLite database,
then change only `DOGAMA_VERSION` and start Compose. Startup applies append-only
migrations before serving requests. Preserve the pre-upgrade database copy and
all player/backup roots. If startup or validation fails, stop the new containers,
restore the database copy, select the prior image version and start again. Never
roll back only the database while a newer application is writing to it.
+3 -1
View File
@@ -1650,11 +1650,13 @@ func (s *server) problem(w http.ResponseWriter, status int, message string) {
func (s *server) securityHeaders(next http.Handler) http.Handler { func (s *server) securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Security-Policy", "default-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'") w.Header().Set("Content-Security-Policy", "default-src 'none'; base-uri 'none'; connect-src 'self'; form-action 'self'; frame-ancestors 'none'; img-src 'self'; script-src 'self'; style-src 'self'")
w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("Referrer-Policy", "no-referrer")
w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()") w.Header().Set("Permissions-Policy", "camera=(), microphone=(), geolocation=()")
w.Header().Set("Cross-Origin-Opener-Policy", "same-origin")
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
next.ServeHTTP(w, r) next.ServeHTTP(w, r)
}) })
} }
+1 -1
View File
@@ -586,7 +586,7 @@ func TestBootstrapAuthenticationAndLogoutFlow(t *testing.T) {
if !strings.Contains(home.Body.String(), "Signed in as <strong>admin</strong>") { if !strings.Contains(home.Body.String(), "Signed in as <strong>admin</strong>") {
t.Fatalf("protected page did not identify user: %s", home.Body.String()) t.Fatalf("protected page did not identify user: %s", home.Body.String())
} }
if home.Header().Get("Content-Security-Policy") == "" || home.Header().Get("X-Content-Type-Options") != "nosniff" { if home.Header().Get("Content-Security-Policy") == "" || home.Header().Get("X-Content-Type-Options") != "nosniff" || home.Header().Get("Strict-Transport-Security") == "" || home.Header().Get("Cross-Origin-Opener-Policy") != "same-origin" {
t.Fatal("security headers missing") t.Fatal("security headers missing")
} }
+164
View File
@@ -0,0 +1,164 @@
//go:build linux
package e2e_test
import (
"bytes"
"context"
"io"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
"time"
)
func TestV1BootstrapAuthenticationAndHTTPBoundary(t *testing.T) {
repositoryRoot := filepath.Clean(filepath.Join("..", ".."))
binary := filepath.Join(t.TempDir(), "dogama")
build := exec.Command("go", "build", "-trimpath", "-o", binary, "./cmd/dogama")
build.Dir = repositoryRoot
if output, err := build.CombinedOutput(); err != nil {
t.Fatalf("build dogama: %v\n%s", err, output)
}
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
address := listener.Addr().String()
_ = listener.Close()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
var logs bytes.Buffer
command := exec.CommandContext(ctx, binary)
command.Env = append(os.Environ(),
"DOGAMA_LISTEN_ADDRESS="+address,
"DOGAMA_DATABASE_PATH="+filepath.Join(t.TempDir(), "dogama.db"),
"DOGAMA_IMPORTS_ROOT="+filepath.Join(t.TempDir(), "imports"),
"DOGAMA_SERVERS_ROOT="+filepath.Join(t.TempDir(), "servers"),
)
command.Stdout, command.Stderr = &logs, &logs
if err := command.Start(); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
cancel()
_ = command.Wait()
})
client := &http.Client{Timeout: 2 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
baseURL := "http://" + address
setup := waitFor(t, client, baseURL+"/setup", &logs)
assertStatus(t, setup, http.StatusOK)
assertSecurityHeaders(t, setup)
csrf := cookieValue(t, setup, "dogama_csrf")
badCSRF := form(t, client, baseURL+"/setup", url.Values{"csrf_token": {"forged"}, "username": {"admin"}, "password": {"correct horse battery staple"}}, csrf)
assertStatus(t, badCSRF, http.StatusForbidden)
created := form(t, client, baseURL+"/setup", url.Values{"csrf_token": {csrf}, "username": {"admin"}, "password": {"correct horse battery staple"}}, csrf)
assertStatus(t, created, http.StatusSeeOther)
closed := get(t, client, baseURL+"/setup", "")
assertStatus(t, closed, http.StatusSeeOther)
loginPage := get(t, client, baseURL+"/login", "")
csrf = cookieValue(t, loginPage, "dogama_csrf")
login := form(t, client, baseURL+"/login", url.Values{"csrf_token": {csrf}, "username": {"admin"}, "password": {"correct horse battery staple"}}, csrf)
assertStatus(t, login, http.StatusSeeOther)
session := cookieValue(t, login, "dogama_session")
csrf = cookieValue(t, login, "dogama_csrf")
unauthenticated := get(t, client, baseURL+"/api/v1/catalog", "")
assertStatus(t, unauthenticated, http.StatusUnauthorized)
authenticated := get(t, client, baseURL+"/api/v1/catalog", "dogama_session="+session+"; dogama_csrf="+csrf)
assertStatus(t, authenticated, http.StatusOK)
if !strings.Contains(readBody(t, authenticated), "palworld-official") {
t.Fatal("authenticated catalog omitted the Palworld reference template")
}
}
func waitFor(t *testing.T, client *http.Client, target string, logs *bytes.Buffer) *http.Response {
t.Helper()
deadline := time.Now().Add(15 * time.Second)
for time.Now().Before(deadline) {
response, err := client.Get(target)
if err == nil {
return response
}
time.Sleep(50 * time.Millisecond)
}
t.Fatalf("application did not start:\n%s", logs.String())
return nil
}
func get(t *testing.T, client *http.Client, target, cookie string) *http.Response {
t.Helper()
request, _ := http.NewRequest(http.MethodGet, target, nil)
if cookie != "" {
request.Header.Set("Cookie", cookie)
}
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func form(t *testing.T, client *http.Client, target string, values url.Values, csrf string) *http.Response {
t.Helper()
request, _ := http.NewRequest(http.MethodPost, target, strings.NewReader(values.Encode()))
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("Cookie", "dogama_csrf="+csrf)
response, err := client.Do(request)
if err != nil {
t.Fatal(err)
}
return response
}
func cookieValue(t *testing.T, response *http.Response, name string) string {
t.Helper()
for _, cookie := range response.Cookies() {
if cookie.Name == name {
if !cookie.Secure || !cookie.HttpOnly || cookie.SameSite != http.SameSiteStrictMode {
t.Fatalf("insecure %s cookie: %#v", name, cookie)
}
return cookie.Value
}
}
t.Fatalf("cookie %s not found", name)
return ""
}
func assertSecurityHeaders(t *testing.T, response *http.Response) {
t.Helper()
for name, expected := range map[string]string{"X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", "Cross-Origin-Opener-Policy": "same-origin"} {
if got := response.Header.Get(name); got != expected {
t.Fatalf("%s = %q, want %q", name, got, expected)
}
}
if !strings.Contains(response.Header.Get("Content-Security-Policy"), "default-src 'none'") || response.Header.Get("Strict-Transport-Security") == "" {
t.Fatal("strict browser security headers are incomplete")
}
}
func assertStatus(t *testing.T, response *http.Response, expected int) {
t.Helper()
if response.StatusCode != expected {
t.Fatalf("status = %d, want %d; body=%s", response.StatusCode, expected, readBody(t, response))
}
}
func readBody(t *testing.T, response *http.Response) string {
t.Helper()
defer response.Body.Close()
body, err := io.ReadAll(response.Body)
if err != nil {
t.Fatal(err)
}
return string(body)
}
+33
View File
@@ -0,0 +1,33 @@
#!/bin/sh
set -eu
version=${1:-}
case "$version" in
v[0-9]*.[0-9]*.[0-9]*) ;;
*) echo "usage: $0 vMAJOR.MINOR.PATCH" >&2; exit 2 ;;
esac
commit=$(git rev-parse HEAD)
epoch=${SOURCE_DATE_EPOCH:-$(git show -s --format=%ct HEAD)}
release_dir="dist/dogama-${version}"
if [ -e "$release_dir" ]; then
echo "$release_dir already exists; refusing to overwrite it" >&2
exit 1
fi
mkdir -p "$release_dir"
for arch in amd64 arm64; do
stage="$release_dir/linux-$arch"
mkdir -p "$stage"
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama" ./cmd/dogama
CGO_ENABLED=0 GOOS=linux GOARCH=$arch go build -trimpath -buildvcs=false \
-ldflags="-s -w -X main.version=$version -X main.commit=$commit" -o "$stage/dogama-agent" ./cmd/dogama-agent
go version -m "$stage/dogama" > "$stage/build-info.txt"
tar --sort=name --owner=0 --group=0 --numeric-owner --mtime="@$epoch" \
-C "$release_dir" -czf "$release_dir/dogama-${version}-linux-$arch.tar.gz" "linux-$arch"
done
go list -m -json all | SOURCE_DATE_EPOCH=$epoch go run ./tools/sbom > "$release_dir/dogama-${version}.spdx.json"
(cd "$release_dir" && sha256sum ./*.tar.gz ./*.spdx.json > SHA256SUMS)
echo "release artifacts written to $release_dir"
+97
View File
@@ -0,0 +1,97 @@
// Command sbom converts `go list -m -json all` output into a deterministic SPDX 2.3 inventory.
package main
import (
"crypto/sha256"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"os"
"strconv"
"strings"
"time"
)
type module struct {
Path string
Version string
Sum string
}
type document struct {
SPDXVersion string `json:"spdxVersion"`
DataLicense string `json:"dataLicense"`
SPDXID string `json:"SPDXID"`
Name string `json:"name"`
DocumentNamespace string `json:"documentNamespace"`
CreationInfo creationInfo `json:"creationInfo"`
Packages []packageInfo `json:"packages"`
}
type creationInfo struct {
Created string `json:"created"`
Creators []string `json:"creators"`
}
type packageInfo struct {
Name string `json:"name"`
SPDXID string `json:"SPDXID"`
VersionInfo string `json:"versionInfo,omitempty"`
DownloadLocation string `json:"downloadLocation"`
FilesAnalyzed bool `json:"filesAnalyzed"`
Checksums []checksum `json:"checksums,omitempty"`
}
type checksum struct {
Algorithm string `json:"algorithm"`
ChecksumValue string `json:"checksumValue"`
}
func main() {
decoder := json.NewDecoder(os.Stdin)
var modules []module
for {
var value module
if err := decoder.Decode(&value); err != nil {
if errors.Is(err, io.EOF) {
break
}
fatal(err)
}
modules = append(modules, value)
}
epoch, err := strconv.ParseInt(os.Getenv("SOURCE_DATE_EPOCH"), 10, 64)
if err != nil {
fatal(fmt.Errorf("SOURCE_DATE_EPOCH: %w", err))
}
digest := sha256.Sum256([]byte(fmt.Sprint(modules)))
doc := document{
SPDXVersion: "SPDX-2.3", DataLicense: "CC0-1.0", SPDXID: "SPDXRef-DOCUMENT",
Name: "DoGaMa Go module inventory", DocumentNamespace: "https://dogama.invalid/spdx/" + hex.EncodeToString(digest[:]),
CreationInfo: creationInfo{Created: time.Unix(epoch, 0).UTC().Format(time.RFC3339), Creators: []string{"Tool: dogama-release"}},
}
for index, value := range modules {
pkg := packageInfo{Name: value.Path, SPDXID: fmt.Sprintf("SPDXRef-Package-%d", index), VersionInfo: value.Version, DownloadLocation: "https://proxy.golang.org/" + strings.ToLower(value.Path), FilesAnalyzed: false}
if strings.HasPrefix(value.Sum, "h1:") {
sum, decodeErr := base64.StdEncoding.DecodeString(strings.TrimPrefix(value.Sum, "h1:"))
if decodeErr != nil || len(sum) != sha256.Size {
fatal(fmt.Errorf("invalid module checksum for %s", value.Path))
}
pkg.Checksums = []checksum{{Algorithm: "SHA256", ChecksumValue: hex.EncodeToString(sum)}}
}
doc.Packages = append(doc.Packages, pkg)
}
encoder := json.NewEncoder(os.Stdout)
encoder.SetIndent("", " ")
if err := encoder.Encode(doc); err != nil {
fatal(err)
}
}
func fatal(err error) {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}