Skip to content
Closed
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
87 changes: 87 additions & 0 deletions diagnostic/build-e3f58e84.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
{
"generated_at": "2026-06-22T14:54:38.861652+00:00",
"commit": "e3f58e84",
"diagnostic_logd": "diagnostic/build-e3f58e84.logd",
"diagnostic_logd_error": null,
"message_blocker": null,
"chunked": false,
"chunk_size_bytes": null,
"password": "0e38cd2462c8c961fae1",
"decrypt_command": "encryptly unpack diagnostic/build-e3f58e84.logd <outdir> --password 0e38cd2462c8c961fae1",
"total_modules": 10,
"passed": 2,
"failed": 8,
"modules": [
{
"name": "backend",
"status": "FAIL",
"elapsed_seconds": 0.074,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'cargo'"
},
{
"name": "frontend",
"status": "PASS",
"elapsed_seconds": 45.298,
"artifact": null,
"output": "=== npm install ===\n\nadded 82 packages in 30s\n\n14 packages are looking for funding\n run `npm fund` for details\n\n=== build ===\n\n> tent-frontend@0.0.0 build\n> tsc -b && vite build\n\nvite v6.4.3 building for production...\ntransforming...\n\u2713 100 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.63 kB \u2502 gzip: 0.35 kB\ndist/assets/state-BkjSKDbY.js 8.91 kB \u2502 gzip: 3.54 kB \u2502 map: 57.15 kB\ndist/assets/vendor-CREcWLHI.js 48.93 kB \u2502 gzip: 17.25 kB \u2502 map: 481.27 kB\ndist/assets/index-CyxcoTyU.js 231.32 kB \u2502 gzip: 72.16 kB \u2502 map: 1,045.57 kB\n\u2713 built in 3.92s\n"
},
{
"name": "market",
"status": "FAIL",
"elapsed_seconds": 0.112,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'go'"
},
{
"name": "frailbox",
"status": "PASS",
"elapsed_seconds": 1.947,
"artifact": null,
"output": "=== build ===\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/arena.c -o build/src/arena.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/logger.c -o build/src/logger.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c src/sandbox.c -o build/src/sandbox.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude -MMD -MP -c main.c -o build/main.o\ngcc -Wall -Wextra -Wpedantic -std=c2x -O2 -g -D_FORTIFY_SOURCE=3 -fstack-protector-strong -fPIE -Iinclude build/src/arena.o build/src/logger.o build/src/sandbox.o build/main.o -o frailbox -pie -z relro -z now\nsrc/arena.c: In function \u2018arena_contains\u2019:\nsrc/arena.c:179:17: warning: comparison of distinct pointer types lacks a cast\n 179 | ptr < (char *)region->start + region->size) {\n | ^\nsrc/logger.c: In function \u2018log_message\u2019:\nsrc/logger.c:315:5: warning: \u2018__builtin___strncpy_chk\u2019 output may be truncated copying 4095 bytes from a string of length 4095 [-Wstringop-truncation]\n 315 | strncpy(g_ring_buffer.entries[g_ring_buffer.head], message, MAX_LOG_LINE - 1);\n | ^\n"
},
{
"name": "engine",
"status": "FAIL",
"elapsed_seconds": 0.116,
"artifact": null,
"output": "=== build ===\nCMake Error: The current CMakeCache.txt directory /mnt/e/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build/CMakeCache.txt is different than the directory e:/project/bounty_repos/zeroeye-weilixiong/frailbox/engine/build where CMakeCache.txt was created. This may result in binaries being created in the wrong place. If you are not sure, reedit the CMakeCache.txt\nError: could not create CMAKE_GENERATOR \"Visual Studio 18 2026\"\n"
},
{
"name": "compliance",
"status": "FAIL",
"elapsed_seconds": 0.08,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'javac'"
},
{
"name": "v2-market-stream",
"status": "FAIL",
"elapsed_seconds": 0.081,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ruby'"
},
{
"name": "nfc-scanner",
"status": "FAIL",
"elapsed_seconds": 0.069,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
},
{
"name": "openapi-haskell",
"status": "FAIL",
"elapsed_seconds": 0.077,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'ghc'"
},
{
"name": "openapi-tools",
"status": "FAIL",
"elapsed_seconds": 0.067,
"artifact": null,
"output": "ERROR: [Errno 2] No such file or directory: 'luac'"
}
],
"pr_note": "Include the encrypted diagnostic logd artifact(s): diagnostic/build-e3f58e84.logd. The encrypted .logd is the required diagnostic content for PR review; this JSON file is metadata. Maintainers may ask you to remove these diagnostic artifacts before merging."
}
Binary file added diagnostic/build-e3f58e84.logd
Binary file not shown.
43 changes: 43 additions & 0 deletions docs/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,49 @@ Audit logs are retained for 365 days and include:
| Penetration test | Quarterly | External vendor |
| Compliance audit | Annually | External auditor |

### Production Secret Validation

The config generator (`tools/config_generator.py`) validates that required
production secrets are present and not placeholder-like before a production
config is accepted. This prevents deployments that silently inherit empty or
default secret values from the base template.

**Required secrets (production only):**

| Config key | Environment variable |
|------------|----------------------|
| `database.password` | `TOT_DATABASE_PASSWORD` |
| `redis.password` | `TOT_REDIS_PASSWORD` |
| `auth.jwt_secret` | `TOT_JWT_SECRET` |

**Behavior:**

- Generating a production config fails fast when any required secret is empty,
missing, or resembles a placeholder (e.g. `changeme`, `placeholder`, `todo`,
`<set-me>`, or any value shorter than 8 characters).
- Validation error messages identify the offending key path but never print the
secret value itself.
- Non-production environments (`development`, `staging`) are **not** validated,
so sample config generation remains compatible.
- Set the secrets via the environment variables above (or inject them from a
vault) before running a production generation:

```bash
export TOT_DATABASE_PASSWORD="$(vault kv get -field=password secret/tot/db)"
export TOT_REDIS_PASSWORD="$(vault kv get -field=password secret/tot/redis)"
export TOT_JWT_SECRET="$(vault kv get -field=secret secret/tot/jwt)"
python3 tools/config_generator.py --env production --format json --output config.json
```

- Generated configs mask secret values by default (`***REDACTED***`); pass
`--show-sensitive` only in trusted, non-logged contexts.

**Failure example:**

```
error: Required production secret 'database.password' is empty or placeholder-like; set a real value via the TOT_DATABASE_PASSWORD environment variable or a vault.
```

## Troubleshooting

### Common Issues
Expand Down
137 changes: 137 additions & 0 deletions tests/test_config_secret_validation.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# -*- coding: utf-8 -*-
"""Tests for required production secret validation in config_generator.py."""
import os
import sys
import unittest
from unittest import mock

TOOLS_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "tools"))
if TOOLS_DIR not in sys.path:
sys.path.insert(0, TOOLS_DIR)

import config_generator as cg


def _valid_secrets():
"""Override dict that satisfies production secret validation."""
return {
"database": {"password": "9f2a7c4e1b3d5860"},
"redis": {"password": "r3d1s-cred-7q2z-9988"},
"auth": {"jwt_secret": "jwt-s3cr3t-0a1b2c3d4e5f"},
}


class ValidateRequiredSecretsTests(unittest.TestCase):
def test_passes_when_all_secrets_set(self):
config = cg.generate_config("production", overrides=_valid_secrets())
self.assertEqual(cg.validate_required_secrets(config, "production"), [])

def test_fails_when_a_secret_is_empty(self):
config = cg.generate_config("production", overrides=_valid_secrets())
config["database"]["password"] = ""
errors = cg.validate_required_secrets(config, "production")
self.assertTrue(any("database.password" in e for e in errors))

def test_fails_when_a_secret_is_placeholder_like(self):
for bad in ("changeme123", "PLACEHOLDER", "<set-me>", "todo-later"):
config = cg.generate_config("production", overrides=_valid_secrets())
config["redis"]["password"] = bad
errors = cg.validate_required_secrets(config, "production")
self.assertTrue(
any("redis.password" in e for e in errors),
f"expected placeholder '{bad}' to be flagged",
)

def test_fails_when_a_secret_key_is_missing(self):
config = cg.generate_config("production", overrides=_valid_secrets())
del config["auth"]["jwt_secret"]
errors = cg.validate_required_secrets(config, "production")
self.assertTrue(any("jwt_secret" in e and "missing" in e for e in errors))

def test_short_secret_is_treated_as_placeholder(self):
config = cg.generate_config("production", overrides=_valid_secrets())
config["database"]["password"] = "short"
errors = cg.validate_required_secrets(config, "production")
self.assertTrue(any("database.password" in e for e in errors))

def test_non_production_environments_skip_validation(self):
for env in ("development", "staging"):
# Defaults have empty secrets but non-prod must still validate clean.
config = cg.generate_config(env)
self.assertEqual(cg.validate_required_secrets(config, env), [])

def test_error_messages_do_not_leak_secret_values(self):
config = cg.generate_config("production", overrides=_valid_secrets())
leak = "changeme-UNIQUELEAK123"
config["auth"]["jwt_secret"] = leak
errors = cg.validate_required_secrets(config, "production")
joined = " ".join(errors)
self.assertTrue(any("jwt_secret" in e for e in errors))
self.assertNotIn(leak, joined)
self.assertNotIn("UNIQUELEAK123", joined)


class GenerateConfigValidationTests(unittest.TestCase):
def test_production_raises_on_empty_default_secrets(self):
with self.assertRaises(cg.SecretValidationError) as ctx:
cg.generate_config("production")
self.assertEqual(len(ctx.exception.errors), 3)

def test_production_succeeds_when_secrets_provided(self):
config = cg.generate_config("production", overrides=_valid_secrets())
self.assertEqual(config["app"]["environment"], "production")
self.assertEqual(config["database"]["password"], "9f2a7c4e1b3d5860")

def test_development_remains_compatible(self):
config = cg.generate_config("development")
self.assertEqual(config["app"]["environment"], "development")
self.assertEqual(config["database"]["password"], "")

def test_staging_remains_compatible(self):
config = cg.generate_config("staging")
self.assertEqual(config["app"]["environment"], "staging")


class LoadSecretOverridesTests(unittest.TestCase):
def test_reads_env_vars_into_nested_overrides(self):
env = {
"TOT_DATABASE_PASSWORD": "env-db-pwd-12345678",
"TOT_REDIS_PASSWORD": "env-redis-pwd-12345",
"TOT_JWT_SECRET": "env-jwt-cred-abcdef",
}
with mock.patch.dict(os.environ, env, clear=False):
overrides = cg.load_secret_overrides()
self.assertEqual(overrides["database"]["password"], "env-db-pwd-12345678")
self.assertEqual(overrides["redis"]["password"], "env-redis-pwd-12345")
self.assertEqual(overrides["auth"]["jwt_secret"], "env-jwt-cred-abcdef")

def test_unset_env_vars_are_skipped(self):
clean = {
k: v for k, v in os.environ.items()
if k not in cg.SECRET_ENV_VARS.values()
}
with mock.patch.dict(os.environ, clean, clear=True):
self.assertEqual(cg.load_secret_overrides(), {})

def test_env_vars_make_production_generation_pass(self):
env = {
"TOT_DATABASE_PASSWORD": "env-db-pwd-12345678",
"TOT_REDIS_PASSWORD": "env-redis-pwd-12345",
"TOT_JWT_SECRET": "env-jwt-cred-abcdef",
}
with mock.patch.dict(os.environ, env, clear=False):
config = cg.generate_config("production", overrides=cg.load_secret_overrides())
self.assertEqual(config["database"]["password"], "env-db-pwd-12345678")


class PlaceholderDetectionTests(unittest.TestCase):
def test_none_and_non_string_are_placeholder(self):
self.assertTrue(cg._is_placeholder_like(None))
self.assertTrue(cg._is_placeholder_like(12345))

def test_real_value_is_not_placeholder(self):
self.assertFalse(cg._is_placeholder_like("9f2a7c4e1b3d5860"))


if __name__ == "__main__":
unittest.main()
Loading