Cross-cutting Concepts

8.1 Threat Model (STRIDE)

Bausteinsicht is a local CLI tool that reads and writes files on the user’s workstation. The primary trust boundary is the filesystem: the tool processes user-supplied JSONC and draw.io files and writes results back to the same directory.

STRIDE Threat Table

ID STRIDE Category Severity Threat Mitigation

T-1

Tampering

Medium

Malicious JSONC or draw.io file in the workspace overwrites model data via sync

Atomic writes (os.Rename after temp-file write, SEC-002); 0600 file permissions (SEC-004); MaxElementDepth=50 prevents stack overflow (SEC-007)

T-2

Tampering

Low

CLI flags --model/--template/--output used to write outside the project directory

Reject .. traversal in all three flags (SEC-001); SafeViewKey() strips path components from view keys used in export filenames (SEC-015); output-dir boundary check (SEC-016)

T-3

Information Disclosure

Low

.bausteinsicht-sync state file exposes element IDs and file paths to co-located processes

File written at 0600; state contains no secrets, only structural metadata (SHA-256 checksums, element IDs)

T-4

Denial of Service

Low

Oversized JSONC model file causes OOM or slow parse

10 MB file size limit enforced before parsing (SEC-006)

T-5

Denial of Service

Low

Deeply nested element hierarchy causes stack overflow during FlattenElements

MaxElementDepth=50 enforced in internal/model/resolve.go:11 (SEC-007); returns error instead of panicking

T-6

Tampering

Low

sanitizeID passes dot and slash characters into draw.io attribute values (XSS-adjacent)

sanitizeID strips dots and slashes (SEC-013); stripTags fixes attribute-value quoting (SEC-008)

Spoofing and Repudiation are not applicable: the tool operates on local files under the user’s own identity; no authentication or audit log is required.

Risk Acceptance

Remaining accepted risks: * T-3 — sync state leaks structural metadata; acceptable because the file lives in the project directory alongside the model itself (same trust level). * Dockerfile script downloads without checksums (SEC-014) — deferred; upstream projects do not publish checksums.

See Security Review 2026-03-01 for the full SEC catalog.

8.2 Security Mitigations

Security controls implemented across the codebase, keyed to threat IDs:

Control Threat Implementation

Atomic file writes

T-1

internal/model/save.go: write to .tmp, then os.Rename (ADR-002)

Path traversal guard

T-2

cmd/bausteinsicht/root.go: validatePaths rejects .. in --model/--template/--output

Safe view key

T-2

internal/export/export.go:SafeViewKey()filepath.Base strips directory components

File permissions

T-1/T-3

All model/sync files written at 0600

File size limit

T-4

internal/model/loader.go: reject JSONC files > 10 MB

Element depth limit

T-5

internal/model/resolve.go:MaxElementDepth=50; FlattenElements returns error on exceeded depth

HTML sanitization

T-6

internal/drawio/label.go:sanitizeID strips ./; stripTags quotes attribute values

Dependency scanning

All

govulncheck in CI (govulncheck job) and Makefile; no CVEs in called code (verified 2026-07-09)

Note
As of 2026-07-09, govulncheck, gitleaks (secret scanning), and go test -race run as blocking CI jobs in .github/workflows/go.yml (govulncheck, gitleaks, test-race), all verified clean against the current codebase (#550). gosec (SAST) and nilaway (nil-pointer analysis) also run in CI but non-blocking (continue-on-error: true) for now: gosec currently reports 35 pre-existing file-permission findings (tracked separately), and nilaway’s memory/time footprint on a GitHub-hosted runner hasn’t been validated as reliably fast/light enough to gate every PR on. `test-race excludes internal/importer/xmi — its ~118MB BigData.xmi test fixture makes race instrumentation’s memory overhead impractical; the other 33 packages are raced normally. See issue #550 for the follow-up to make gosec/nilaway blocking once their respective backlogs are addressed.

8.3 Test Strategy

Decided in ADR-007. Three tiers:

Tier Volume Scope Characteristics

Unit tests

~70%

Pure functions: label parsing, ID resolution, XML element creation, validation

No I/O; <100 ms per test; co-located *_test.go

Integration tests

~25%

Package-level: full model load/save cycle, full XML round-trip

t.TempDir() for fixtures; real I/O; no mocks unless unavoidable; run with -race

E2E tests

~5%

Full CLI workflows: sync, export, add element

Execute compiled binary; real JSONC + draw.io files; <5 s per test

Property-Based Testing

pgregory.net/rapid is used for roundtrip and idempotency properties:

  • Label escaping / unescaping roundtrip (internal/drawio/)

  • escapeHTML / trimBrackets invariants

Test Fixtures

Packages use testdata/ directories for representative input files. Golden-file comparisons are done by committing expected output and checking with bytes.Equal in tests. There is no -update-golden flag; update expected files manually and recommit.

8.4 Observability

User-Facing Output

Normal output goes to stdout:

  • text (default) — human-readable summaries

  • json (--format json) — machine-parseable; used by LLM agents

Warnings and errors go to stderr.

Verbose Mode

With --verbose, additional sync and export progress is printed to stderr (plain text, no [DEBUG] prefix):

Syncing model: architecture.jsonc
  42 elements, 18 relationships, 3 views
Forward sync: 2 elements created, 1 updated, 0 deleted; 3 connectors created, 0 updated, 0 deleted
Reverse sync: 0 elements created, 1 updated, 0 deleted; 0 relationships created, 0 updated, 0 deleted
Conflicts resolved: 0 (model wins)

Verbose output is suppressed in --format json mode to keep stdout machine-parseable.

No Logging Framework

Bausteinsicht uses fmt.Fprintf(stderr, …​) for verbose output and fmt for normal output. No external logging framework is used — log package is not imported in the CLI layer.

8.5 Error Handling

Strategy

Errors propagate upward and are only formatted for the user at the CLI layer (cmd/bausteinsicht/).

Layer Error Handling

internal/*

Return error values with context using fmt.Errorf("loading model: %w", err). Never print to stdout/stderr.

cmd/*

Catch errors, format them for the user (plain text or JSON), and set the exit code via exitWithCode.

Exit Codes

Code Meaning

0

Success

1

Validation error or sync conflict

2

File not found or I/O error

Structured Error Output

With --format json, errors are emitted to stderr as:

{"error": "validation failed: model.webshop.api unknown kind", "code": 1}

The single error string contains the full wrapped error chain. This enables LLM agents to parse errors programmatically.

Note
The details array (multiple per-field errors) is not part of the JSON error format. Validation warnings are only available in text mode via bausteinsicht validate.

8.6 JSONC Comment Preservation

Standard Go encoding/json does not support comments. Bausteinsicht strips comments before parsing and writes back selectively:

  1. Remove single-line comments (// …​) and trailing commas before parsing

  2. Parse the cleaned JSON with encoding/json

  3. On pure insertions (new elements, new relationships): model.PatchInsert / cmd/bausteinsicht/sync.go:saveModel patches only the changed lines, preserving existing comments and key ordering (ADR-011)

  4. On deletions or modifications of existing entries: full rewrite via model.Save — comments are lost

Note
The REPL’s save command follows the same patch-first/full-save-fallback strategy.

8.7 File Atomicity

When writing files, Bausteinsicht uses a write-to-temp-then-rename pattern:

  1. Write to a temporary file in the same directory (e.g., .model.jsonc.tmp)

  2. os.Rename the temp file to the target path (atomic on most filesystems)

  3. On failure, the original file remains intact

This prevents corrupted files if the process is interrupted during write (mitigates T-1).

8.8 Configuration Discovery

Bausteinsicht uses convention over configuration:

  1. Look for *.jsonc in the current directory. Exactly one match is used as the model file; multiple files require --model (the tool never silently picks one, since it mutates synchronized files — SEC-005).

  2. Look for *.drawio in the current directory (the diagram file)

  3. Look for .bausteinsicht-sync in the current directory

  4. Template: template.drawio in the current directory, or fall back to the embedded default

All paths can be overridden via CLI flags (--model, --template).

8.9 Version Management

The binary version is injected at build time via ldflags:

go build -ldflags "-X main.version=0.1.0" ./cmd/bausteinsicht

GoReleaser handles this automatically for tagged releases.