1083 lines
52 KiB
Python
1083 lines
52 KiB
Python
import contextlib
|
|
import hashlib
|
|
import io
|
|
import json
|
|
import os
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
import sys
|
|
import tempfile
|
|
import unittest
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_DIR = ROOT / 'app'
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
import migrate_runtime_safety
|
|
import migrate_observability_db
|
|
import scanner
|
|
from paths import apply_path_config
|
|
from query_policy import QueryPolicyError, validate_rejected_query_policy
|
|
from runtime_security import ensure_private_directory, preflight_lifecycle_paths, private_directory_ready, private_file_ready
|
|
|
|
|
|
class RowCursor:
|
|
def __init__(self, row=None, rows=None):
|
|
self.row = row
|
|
self.rows = list(rows or [])
|
|
|
|
def fetchone(self):
|
|
return self.row
|
|
|
|
def fetchall(self):
|
|
return self.rows
|
|
|
|
|
|
class MigrationAuthorityTests(unittest.TestCase):
|
|
@staticmethod
|
|
def rejected_query_entry(**changes):
|
|
entry = {
|
|
'source': 'dockerhub',
|
|
'query': 'retired',
|
|
'status': 'rejected_zero_alive',
|
|
'evidence_cutoff': '2026-09-07T00:00:00+00:00',
|
|
'successful_scans': 1000,
|
|
'findings': 10,
|
|
'unique_credentials': 2,
|
|
'pending_candidates': 0,
|
|
'ever_alive_credentials': 0,
|
|
'reviewed_queue_rows': 3,
|
|
}
|
|
entry.update(changes)
|
|
return entry
|
|
|
|
def test_hardening_marker_does_not_treat_reused_pid_as_live_owner(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
work_dir = os.path.join(temp_dir, 'work')
|
|
candidate_dir = os.path.join(work_dir, 'trufflehog-run-fixture')
|
|
ensure_private_directory(work_dir, reject_reparse=True)
|
|
ensure_private_directory(candidate_dir, reject_reparse=True)
|
|
marker = os.path.join(candidate_dir, '.scanner-owner.json')
|
|
Path(marker).write_text(json.dumps({
|
|
'owner_pid': 42,
|
|
'owner_creation_time': 'fixture-old-process',
|
|
'owner_executable': os.path.join(temp_dir, 'trufflehog.exe'),
|
|
}), encoding='utf-8')
|
|
scanner.harden_private_file(marker)
|
|
config = {'global': {'work_dir': work_dir}}
|
|
|
|
with mock.patch(
|
|
'process_identity.exact_process_identity_state', return_value='reused',
|
|
), mock.patch.object(migrate_runtime_safety, '_pid_running', return_value=True):
|
|
self.assertEqual(
|
|
migrate_runtime_safety.known_application_process_markers(config),
|
|
[],
|
|
)
|
|
|
|
with mock.patch(
|
|
'process_identity.exact_process_identity_state', return_value='unknown',
|
|
), mock.patch.object(migrate_runtime_safety, '_pid_running', return_value=True):
|
|
self.assertEqual(
|
|
migrate_runtime_safety.known_application_process_markers(config),
|
|
[(42, marker)],
|
|
)
|
|
|
|
def test_production_legacy_spool_placeholder_resolves_to_reviewed_path(self):
|
|
config_path = APP_DIR / 'config.yaml'
|
|
config = migrate_runtime_safety.load_config(str(config_path))
|
|
expected = os.path.normcase(os.path.abspath(r'D:\truf\runtime\result_spool'))
|
|
self.assertEqual(
|
|
os.path.normcase(config['global']['legacy_result_spool_dir']), expected,
|
|
)
|
|
self.assertEqual(
|
|
os.path.normcase(migrate_runtime_safety.reviewed_legacy_result_spool(config)),
|
|
expected,
|
|
)
|
|
|
|
def test_oversized_legacy_backfill_registers_review_without_fetching_payload(self):
|
|
class Connection:
|
|
is_postgres = True
|
|
|
|
def __init__(self):
|
|
self.candidate_calls = 0
|
|
self.sql = []
|
|
|
|
def execute(self, sql, params=None):
|
|
self.sql.append(sql)
|
|
if 'SELECT s.id, octet_length' in sql:
|
|
self.candidate_calls += 1
|
|
return RowCursor(
|
|
{'id': 77, 'payload_bytes': 300 * 1024 * 1024}
|
|
if self.candidate_calls == 1 else None
|
|
)
|
|
if 'SELECT COUNT(*) AS count FROM target_scans' in sql:
|
|
return RowCursor({'count': 1})
|
|
return RowCursor()
|
|
|
|
@staticmethod
|
|
def commit():
|
|
return None
|
|
|
|
@staticmethod
|
|
def rollback():
|
|
return None
|
|
|
|
connection = Connection()
|
|
report = migrate_runtime_safety.backfill_normalized_raw_results(
|
|
SimpleNamespace(conn=connection), max_rows=10,
|
|
max_bytes=512 * 1024 * 1024, max_object_bytes=192 * 1024 * 1024,
|
|
)
|
|
self.assertEqual(report['reviewed_oversized'], 1)
|
|
self.assertFalse(any('SELECT id, raw_result_json' in sql for sql in connection.sql))
|
|
self.assertTrue(any("'legacy_payload_oversized'" in sql for sql in connection.sql))
|
|
|
|
def test_reconstruction_sql_never_selects_unbounded_target_scan_blob(self):
|
|
source = (APP_DIR / 'scanner_db.py').read_text(encoding='utf-8')
|
|
start = source.index(' def projection_scan_header(')
|
|
end = source.index('\n def iter_projection_findings(', start)
|
|
method = source[start:end]
|
|
self.assertNotIn('SELECT ts.*', method)
|
|
self.assertIn('raw_result_bytes', method)
|
|
self.assertIn("'OCTET_LENGTH(raw_result_json)'", method)
|
|
self.assertIn('AND {payload_size_sql} = ?', method)
|
|
|
|
def setUp(self):
|
|
self.cluster_lock = mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'ClusterAuthorityLock',
|
|
return_value=contextlib.nullcontext(),
|
|
)
|
|
self.preflight = mock.patch.object(migrate_runtime_safety, 'preflight_lifecycle_paths')
|
|
self.cluster_lock.start()
|
|
self.preflight.start()
|
|
|
|
def tearDown(self):
|
|
self.preflight.stop()
|
|
self.cluster_lock.stop()
|
|
|
|
def args(self, **changes):
|
|
values = {
|
|
'config': 'config.yaml', 'database_url': None, 'sqlite': None,
|
|
'initialize_base': False, 'todo': None, 'source': None, 'platform': None,
|
|
'query': 'offline', 'max_rows': 10, 'max_bytes': 1024, 'max_seconds': 1,
|
|
'until_complete': False, 'max_batches': 10, 'resolve_issue': [],
|
|
'harden_runtime': False, 'reconcile_jsonl_projections': False,
|
|
'resolve_jsonl_issue': [],
|
|
'resolve_jsonl_issue_manifest': None,
|
|
'resolve_jsonl_conflict_manifest': None,
|
|
'recover_dead_scan_slots': False,
|
|
'repair_docker_timeout_attempts': False,
|
|
'cold_stale_backlog': False,
|
|
'reactivate_cold_backlog': False,
|
|
'policy_manifest': None,
|
|
'policy_source': None,
|
|
'policy_platform': None,
|
|
'policy_query': None,
|
|
'dry_run': False,
|
|
'apply': True, 'sources_stopped': True,
|
|
}
|
|
values.update(changes)
|
|
return SimpleNamespace(**values)
|
|
|
|
def test_target_queue_query_policy_preserves_exact_case(self):
|
|
report = migrate_runtime_safety.configured_target_queue_query_policy({
|
|
'sources': {
|
|
'dockerhub': {'queries': ['openai', 'OPENAI_API_KEY']},
|
|
'github_actions': {'enabled': False, 'queries': ['logs']},
|
|
},
|
|
})
|
|
self.assertEqual(
|
|
report['queries'][('dockerhub', 'docker')],
|
|
('openai', 'OPENAI_API_KEY'),
|
|
)
|
|
self.assertEqual(report['queries'][('github_actions', 'github_actions')], ('logs',))
|
|
self.assertRegex(report['policy_sha256'], r'^[a-f0-9]{64}$')
|
|
|
|
def test_rejected_query_policy_is_exact_and_source_specific(self):
|
|
config = {
|
|
'sources': {
|
|
'dockerhub': {'queries': ['kept']},
|
|
'npm': {'queries': ['retired']},
|
|
},
|
|
'query_policy': {
|
|
'rejected': [self.rejected_query_entry()],
|
|
},
|
|
}
|
|
entries = validate_rejected_query_policy(config)
|
|
self.assertEqual(
|
|
[(entry['source'], entry['query']) for entry in entries],
|
|
[('dockerhub', 'retired')],
|
|
)
|
|
apply_path_config(config)
|
|
report = migrate_runtime_safety.configured_target_queue_query_policy(config)
|
|
self.assertEqual(report['rejected'][('dockerhub', 'docker')], ('retired',))
|
|
self.assertEqual(report['queries'][('npm', 'npm')], ('retired',))
|
|
self.assertNotIn(('npm', 'npm'), report['rejected'])
|
|
self.assertTrue(report['rejected_registry_present'])
|
|
|
|
def test_rejected_query_policy_fails_closed_on_invalid_evidence(self):
|
|
base = {
|
|
'sources': {'dockerhub': {'queries': ['kept']}},
|
|
'query_policy': {'rejected': [self.rejected_query_entry()]},
|
|
}
|
|
invalid_entries = (
|
|
self.rejected_query_entry(successful_scans=999),
|
|
self.rejected_query_entry(pending_candidates=1),
|
|
self.rejected_query_entry(ever_alive_credentials=1),
|
|
self.rejected_query_entry(evidence_cutoff='2026-09-07T00:00:00'),
|
|
{**self.rejected_query_entry(), 'unexpected': 1},
|
|
)
|
|
for entry in invalid_entries:
|
|
with self.subTest(entry=entry):
|
|
config = {**base, 'query_policy': {'rejected': [entry]}}
|
|
with self.assertRaises(QueryPolicyError):
|
|
validate_rejected_query_policy(config)
|
|
|
|
def test_rejected_query_policy_rejects_overlap_duplicate_and_sentinel(self):
|
|
overlap = {
|
|
'sources': {'dockerhub': {'queries': ['retired']}},
|
|
'query_policy': {'rejected': [self.rejected_query_entry()]},
|
|
}
|
|
with self.assertRaisesRegex(QueryPolicyError, 'overlap'):
|
|
validate_rejected_query_policy(overlap)
|
|
|
|
duplicate = {
|
|
'sources': {'dockerhub': {'queries': ['kept']}},
|
|
'query_policy': {
|
|
'rejected': [self.rejected_query_entry(), self.rejected_query_entry()],
|
|
},
|
|
}
|
|
with self.assertRaisesRegex(QueryPolicyError, 'duplicate'):
|
|
validate_rejected_query_policy(duplicate)
|
|
|
|
sentinel = {
|
|
'sources': {'huggingface': {'queries': ['kept']}},
|
|
'query_policy': {'rejected': [self.rejected_query_entry(
|
|
source='huggingface', query='spaces',
|
|
)]},
|
|
}
|
|
with self.assertRaisesRegex(QueryPolicyError, 'sentinel'):
|
|
validate_rejected_query_policy(sentinel)
|
|
|
|
def test_rejected_registry_without_scope_selects_nothing(self):
|
|
db = mock.Mock()
|
|
policy = {
|
|
'queries': {('npm', 'npm'): ('kept',)},
|
|
'rejected': {('dockerhub', 'docker'): ('retired',)},
|
|
'rejected_registry_present': True,
|
|
}
|
|
self.assertEqual(
|
|
migrate_runtime_safety._stale_target_queue_rows(
|
|
db, policy, 'npm', 'npm', 10,
|
|
),
|
|
([], []),
|
|
)
|
|
db.conn.execute.assert_not_called()
|
|
|
|
def test_main_requires_postgres_dsn_unless_sqlite_is_explicit(self):
|
|
config = {'global': {'database_path': r'D:\scanner_active.db'}, 'supervisor': {}}
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=self.args()), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment', return_value=None), \
|
|
mock.patch.dict(os.environ, {'SCANNER_DB_URL': '', 'DATABASE_URL': ''}):
|
|
with self.assertRaisesRegex(SystemExit, 'PostgreSQL DSN is required'):
|
|
migrate_runtime_safety.main()
|
|
|
|
def test_migration_preflight_uses_server_authority_without_trufflehog(self):
|
|
config = {'global': {}, 'supervisor': {}}
|
|
args = self.args()
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=args), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment'), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety, 'database_url_from_env',
|
|
return_value='postgresql://truf:fixture@127.0.0.1:5432/truf',
|
|
), mock.patch.object(
|
|
migrate_runtime_safety, 'preflight_lifecycle_paths',
|
|
side_effect=OSError('fixture stop'),
|
|
) as preflight:
|
|
with self.assertRaisesRegex(OSError, 'fixture stop'):
|
|
migrate_runtime_safety.main()
|
|
preflight.assert_called_once_with(
|
|
os.path.abspath('config.yaml'), config, authority_profile='server',
|
|
)
|
|
|
|
def test_dead_scan_slot_recovery_deletes_only_definitively_dead_identities(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
state_dir = os.path.join(temp_dir, 'state')
|
|
ensure_private_directory(state_dir, reject_reparse=True)
|
|
path = os.path.join(state_dir, 'scan_limiter.db')
|
|
connection = sqlite3.connect(path)
|
|
connection.executescript(scanner.SCAN_SLOT_SCHEMA)
|
|
rows = (
|
|
('dead', 101, 201),
|
|
('reused', 102, 202),
|
|
('live-owner', 103, 203),
|
|
('live-child', 104, 204),
|
|
('uncertain', 105, 205),
|
|
('failure', 106, 206),
|
|
)
|
|
for index, (slot_id, owner_pid, child_pid) in enumerate(rows):
|
|
connection.execute(
|
|
'''INSERT INTO scan_slots (
|
|
slot_id, owner_pid, owner_thread, owner_source,
|
|
owner_creation_time, owner_executable, child_pid,
|
|
child_creation_time, child_executable, command,
|
|
acquired_at, updated_at
|
|
) VALUES (?, ?, ?, 'fixture', ?, 'owner.exe', ?, ?, 'child.exe', '', ?, ?)''',
|
|
(
|
|
slot_id, owner_pid, index + 1, f'owner-{slot_id}',
|
|
child_pid, f'child-{slot_id}', 100.0 + index, 200.0 + index,
|
|
),
|
|
)
|
|
connection.commit()
|
|
connection.close()
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
|
|
def identity_live(pid, creation_time, executable):
|
|
if pid in (101, 201, 102, 202):
|
|
return False
|
|
if pid == 103:
|
|
return True
|
|
if pid == 204:
|
|
return True
|
|
if pid == 105:
|
|
return None
|
|
if pid == 106:
|
|
raise OSError('identity lookup denied')
|
|
return False
|
|
|
|
report = migrate_runtime_safety.recover_dead_scan_slots(
|
|
{'global': {'state_dir': state_dir, 'scan_limiter_db': path}},
|
|
identity_live=identity_live,
|
|
now=1000.0,
|
|
)
|
|
self.assertEqual(report['deleted'], 2)
|
|
self.assertEqual(report['remaining'], 4)
|
|
deleted = {row['slot_id'] for row in report['rows'] if row['deleted']}
|
|
self.assertEqual(deleted, {'dead', 'reused'})
|
|
retained = {row['slot_id'] for row in report['rows'] if not row['deleted']}
|
|
self.assertEqual(retained, {'live-owner', 'live-child', 'uncertain', 'failure'})
|
|
with contextlib.closing(sqlite3.connect(path)) as verify:
|
|
remaining = {row[0] for row in verify.execute('SELECT slot_id FROM scan_slots')}
|
|
self.assertEqual(remaining, retained)
|
|
|
|
def test_reused_pid_with_different_creation_identity_is_dead(self):
|
|
identity = SimpleNamespace(creation_time='new-creation', executable=r'C:\fixture\owner.exe')
|
|
process = mock.Mock(identity=identity)
|
|
process.is_running.return_value = True
|
|
with mock.patch.object(scanner, 'open_process', return_value=process):
|
|
self.assertFalse(scanner.exact_process_identity_live(
|
|
1234, 'old-creation', r'C:\fixture\owner.exe',
|
|
))
|
|
process.close.assert_called_once_with()
|
|
|
|
def test_recover_dead_scan_slots_cli_uses_shared_authority_without_postgres_stop(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
state_dir = os.path.join(temp_dir, 'state')
|
|
ensure_private_directory(state_dir, reject_reparse=True)
|
|
path = os.path.join(state_dir, 'scan_limiter.db')
|
|
connection = sqlite3.connect(path)
|
|
connection.executescript(scanner.SCAN_SLOT_SCHEMA)
|
|
connection.execute(
|
|
'''INSERT INTO scan_slots (
|
|
slot_id, owner_pid, owner_thread, owner_source,
|
|
owner_creation_time, owner_executable, child_pid,
|
|
child_creation_time, child_executable, command,
|
|
acquired_at, updated_at
|
|
) VALUES ('dead', 99999991, 1, 'fixture', 'old-owner', 'owner.exe',
|
|
99999992, 'old-child', 'child.exe', '', 1, 2)'''
|
|
)
|
|
connection.commit()
|
|
connection.close()
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
config = {
|
|
'global': {
|
|
'root_dir': temp_dir,
|
|
'state_dir': state_dir,
|
|
'scan_limiter_db': path,
|
|
},
|
|
'supervisor': {},
|
|
}
|
|
args = self.args(recover_dead_scan_slots=True)
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=args), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment'), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'database_url_from_env',
|
|
return_value='postgresql://truf:fixture@127.0.0.1:5432/truf',
|
|
), \
|
|
mock.patch.object(migrate_runtime_safety, 'require_local_sources_stopped') as stopped:
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
stopped.assert_called_once_with(config, inspect_scan_slots=False)
|
|
with contextlib.closing(sqlite3.connect(path)) as verify:
|
|
self.assertEqual(verify.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0], 0)
|
|
|
|
def test_sqlite_one_batch_is_nonzero_until_bounded_reconciliation_completes(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
runtime = os.path.join(temp_dir, 'runtime')
|
|
config = {
|
|
'global': {
|
|
'root_dir': temp_dir,
|
|
'runtime_dir': runtime,
|
|
'state_dir': os.path.join(runtime, 'state'),
|
|
'log_dir': os.path.join(runtime, 'logs'),
|
|
'control_dir': os.path.join(runtime, 'control'),
|
|
},
|
|
'supervisor': {
|
|
'instance_file': os.path.join(runtime, 'control', 'supervisor.instance.json'),
|
|
'log_dir': os.path.join(runtime, 'logs'),
|
|
},
|
|
}
|
|
todo = os.path.join(temp_dir, 'todo.txt')
|
|
Path(todo).write_text(
|
|
'https://github.com/example/one\nhttps://github.com/example/two\n',
|
|
encoding='ascii',
|
|
)
|
|
db_path = os.path.join(temp_dir, 'migration.sqlite')
|
|
one_batch = self.args(
|
|
sqlite=db_path, initialize_base=True, todo=todo,
|
|
source='source', platform='github', max_rows=1,
|
|
)
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=one_batch), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config):
|
|
self.assertEqual(migrate_runtime_safety.main(), 2)
|
|
until_complete = self.args(
|
|
sqlite=db_path, todo=todo, source='source', platform='github',
|
|
max_rows=1, until_complete=True, max_batches=10,
|
|
)
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=until_complete), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config):
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
|
|
def test_online_identity_checks_every_bound_cluster_field(self):
|
|
identity = {
|
|
'database': 'truf', 'user': 'truf', 'port': 5432,
|
|
'data_directory': r'D:\bound-data', 'system_identifier': '12345',
|
|
}
|
|
row = {
|
|
'database': 'truf', 'user_name': 'truf', 'port': 5432,
|
|
'data_directory': r'D:\bound-data', 'system_identifier': '12345',
|
|
}
|
|
|
|
class Connection:
|
|
def execute(self, sql, params=None):
|
|
return RowCursor(row=row)
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
db = SimpleNamespace(conn=Connection(), url=None)
|
|
dsn = 'postgresql://truf:secret@127.0.0.1:5432/truf'
|
|
with mock.patch.object(migrate_runtime_safety, 'verify_cluster_identity', return_value=identity):
|
|
found, canonical = migrate_runtime_safety._online_postgres_identity(db, dsn, {})
|
|
self.assertEqual(found, identity)
|
|
self.assertEqual(canonical, dsn)
|
|
|
|
bad = dict(row, system_identifier='99999')
|
|
db.conn.execute = lambda sql, params=None: RowCursor(row=bad)
|
|
with mock.patch.object(migrate_runtime_safety, 'verify_cluster_identity', return_value=identity):
|
|
with self.assertRaisesRegex(RuntimeError, 'system_identifier'):
|
|
migrate_runtime_safety._online_postgres_identity(db, dsn, {})
|
|
|
|
def test_postgres_guard_sets_migration_timeouts_and_holds_advisory_lock(self):
|
|
statements = []
|
|
|
|
class Connection:
|
|
def execute(self, sql, params=None):
|
|
statements.append(sql)
|
|
if 'current_schema()' in sql:
|
|
return RowCursor(row={
|
|
'schema_name': 'public',
|
|
'search_path': 'public',
|
|
'public_create': False,
|
|
})
|
|
if 'pg_stat_activity' in sql:
|
|
return RowCursor(rows=[])
|
|
if 'pg_try_advisory_lock' in sql:
|
|
return RowCursor(row={'locked': True})
|
|
return RowCursor(row={'pg_advisory_unlock': True})
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
def rollback(self):
|
|
pass
|
|
|
|
with migrate_runtime_safety.postgres_migration_guard(SimpleNamespace(conn=Connection())):
|
|
statements.append('MIGRATION BODY')
|
|
self.assertLess(statements.index('SET statement_timeout = 0'), statements.index('MIGRATION BODY'))
|
|
self.assertTrue(any('lock_timeout' in sql for sql in statements))
|
|
self.assertTrue(any('pg_try_advisory_lock' in sql for sql in statements))
|
|
self.assertTrue(any('pg_advisory_unlock' in sql for sql in statements))
|
|
|
|
def test_redirecting_migration_dsn_is_rejected_before_scanner_db_construction(self):
|
|
config = {'global': {'root_dir': r'D:\bound'}, 'supervisor': {}}
|
|
identity = {'database': 'truf', 'user': 'truf', 'port': 5432}
|
|
attacks = (
|
|
'postgresql://truf:secret@127.0.0.1:5432/truf?host=attacker',
|
|
'postgresql://truf:secret@127.0.0.1:5432,attacker:5432/truf',
|
|
'postgresql://other:secret@127.0.0.1:5432/truf',
|
|
)
|
|
for attack in attacks:
|
|
with self.subTest(attack=attack), \
|
|
mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=self.args(database_url=attack)), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment'), \
|
|
mock.patch.object(migrate_runtime_safety, 'require_local_sources_stopped'), \
|
|
mock.patch.object(migrate_runtime_safety, 'verify_cluster_identity', return_value=identity), \
|
|
mock.patch.object(migrate_runtime_safety, 'ScannerDB') as scanner_db:
|
|
with self.assertRaisesRegex(RuntimeError, 'authority validation'):
|
|
migrate_runtime_safety.main()
|
|
scanner_db.assert_not_called()
|
|
|
|
def test_hardening_action_never_constructs_a_database(self):
|
|
config = {'global': {'root_dir': r'D:\bound'}, 'supervisor': {}}
|
|
with mock.patch.object(
|
|
migrate_runtime_safety, 'parse_args', return_value=self.args(harden_runtime=True)
|
|
), mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'database_url_from_env', return_value='postgresql://truf:fixture@127.0.0.1:5432/truf'), \
|
|
mock.patch.object(migrate_runtime_safety, 'require_runtime_hardening_stopped'), \
|
|
mock.patch.object(migrate_runtime_safety, 'find_postgres_environment_path', return_value=None), \
|
|
mock.patch.object(migrate_runtime_safety, 'harden_runtime_paths', return_value=3), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment') as load_environment, \
|
|
mock.patch.object(migrate_runtime_safety.sqlite3, 'connect') as sqlite_connect, \
|
|
mock.patch.object(migrate_runtime_safety, 'ScannerDB') as scanner_db:
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
load_environment.assert_called_once_with(os.path.abspath('config.yaml'), config)
|
|
sqlite_connect.assert_not_called()
|
|
scanner_db.assert_not_called()
|
|
|
|
def test_hardening_cli_loads_private_configured_postgres_environment_without_exposing_dsn(self):
|
|
secret = 'fixture-secret-not-for-output'
|
|
dsn = f'postgresql://truf:{secret}@127.0.0.1:5432/truf'
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
project_dir = os.path.join(temp_dir, 'app')
|
|
ensure_private_directory(project_dir, reject_reparse=True)
|
|
config_path = os.path.join(project_dir, 'config.yaml')
|
|
env_path = os.path.join(temp_dir, '.env.postgres')
|
|
Path(config_path).write_text(
|
|
f'global:\n root_dir: "{Path(temp_dir).as_posix()}"\n project_dir: "."\n',
|
|
encoding='ascii',
|
|
)
|
|
Path(env_path).write_text(f'DATABASE_URL={dsn}\n', encoding='ascii')
|
|
migrate_runtime_safety.harden_private_file(env_path)
|
|
self.assertTrue(private_file_ready(env_path))
|
|
argv = [
|
|
'migrate_runtime_safety.py', '--config', config_path,
|
|
'--harden-runtime', '--apply', '--sources-stopped',
|
|
]
|
|
stdout = io.StringIO()
|
|
stderr = io.StringIO()
|
|
cleared_environment = {
|
|
key: '' for key in (
|
|
'TRUF_MANAGED_POSTGRES_DSN', 'SCANNER_DB_URL', 'DATABASE_URL',
|
|
'TRUF_POSTGRES_PASSWORD', 'TRUF_POSTGRES_DB',
|
|
'TRUF_POSTGRES_USER', 'TRUF_POSTGRES_PORT',
|
|
)
|
|
}
|
|
with mock.patch.object(sys, 'argv', argv), \
|
|
mock.patch.dict(os.environ, cleared_environment), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'ClusterAuthorityLock',
|
|
return_value=contextlib.nullcontext(),
|
|
) as authority_lock, \
|
|
mock.patch.object(migrate_runtime_safety, 'require_runtime_hardening_stopped'), \
|
|
mock.patch.object(migrate_runtime_safety, 'harden_runtime_paths', return_value=3) as harden_paths, \
|
|
contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
|
|
authority_lock.assert_called_once()
|
|
self.assertEqual(authority_lock.call_args.kwargs['endpoint_dsn'], dsn)
|
|
self.assertEqual(harden_paths.call_args.args[1], os.path.abspath(env_path))
|
|
self.assertNotIn(secret, ' '.join(argv))
|
|
self.assertNotIn(secret, stdout.getvalue())
|
|
self.assertNotIn(secret, stderr.getvalue())
|
|
|
|
def test_jsonl_projection_cli_builds_ledgers_without_rewriting_history(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
results_dir = os.path.join(temp_dir, 'results')
|
|
ensure_private_directory(results_dir, reject_reparse=True)
|
|
scan_first = b'{"scan_event_id":"event-1"}\n'
|
|
scan_corrupt = b'1, "legacy":"reviewed"}\n'
|
|
scan_last = b'{"scan_event_id":"event-2"}\n'
|
|
payloads = {
|
|
'scan_results.jsonl': (scan_first + scan_corrupt + scan_last, 'scan_event_id'),
|
|
'found_secrets.jsonl': (b'{"finding_uid":"finding-1"}\n', 'finding_uid'),
|
|
'scan_errors.log': (b'event-1:1\twhen\tgithub\ttarget\terror\n', 'error_row_id'),
|
|
}
|
|
for name, (payload, _) in payloads.items():
|
|
path = os.path.join(results_dir, name)
|
|
Path(path).write_bytes(payload)
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
manifest_path = os.path.join(temp_dir, 'review-manifest.json')
|
|
Path(manifest_path).write_text(json.dumps({
|
|
'schema': 1,
|
|
'type': 'truf-jsonl-projection-review',
|
|
'audit_sha256': hashlib.sha256(b'fixture-audit').hexdigest(),
|
|
'issues': [{
|
|
'ledger_kind': 'scan_results',
|
|
'file': 'scan_results.jsonl',
|
|
'offset': len(scan_first),
|
|
'length': len(scan_corrupt),
|
|
'sha256': hashlib.sha256(scan_corrupt).hexdigest(),
|
|
'classification': 'legacy_numeric_prefix_corrupt_json',
|
|
'safe_identity': False,
|
|
}],
|
|
}), encoding='ascii')
|
|
migrate_runtime_safety.harden_private_file(manifest_path)
|
|
config = {
|
|
'global': {
|
|
'root_dir': temp_dir,
|
|
'runtime_dir': os.path.join(temp_dir, 'runtime'),
|
|
'results_dir': results_dir,
|
|
},
|
|
'supervisor': {},
|
|
}
|
|
args = self.args(
|
|
reconcile_jsonl_projections=True,
|
|
until_complete=True,
|
|
max_rows=1,
|
|
max_bytes=1024,
|
|
max_seconds=1,
|
|
max_batches=10,
|
|
resolve_jsonl_issue_manifest=manifest_path,
|
|
)
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=args), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment'), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'database_url_from_env',
|
|
return_value='postgresql://truf:fixture@127.0.0.1:5432/truf',
|
|
), \
|
|
mock.patch.object(migrate_runtime_safety, 'require_runtime_hardening_stopped'):
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
for name, (payload, identity_key) in payloads.items():
|
|
path = os.path.join(results_dir, name)
|
|
self.assertEqual(Path(path).read_bytes(), payload)
|
|
ledger_path = os.path.splitext(path)[0] + '.publication-ledger.sqlite3'
|
|
with contextlib.closing(sqlite3.connect(ledger_path)) as ledger:
|
|
marker = ledger.execute(
|
|
'SELECT value FROM publication_meta WHERE key = ?',
|
|
(f'bootstrapped:{identity_key}',),
|
|
).fetchone()
|
|
self.assertEqual(marker, ('1',))
|
|
self.assertTrue(private_file_ready(ledger_path))
|
|
|
|
def test_v2_projection_cursors_start_at_existing_active_file_sizes(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
results_dir = os.path.join(temp_dir, 'results')
|
|
ensure_private_directory(results_dir, reject_reparse=True)
|
|
for name, payload in (
|
|
('scan_results.jsonl', b'{"legacy":1}\n'),
|
|
('found_secrets.jsonl', b'{"legacy":2}\n'),
|
|
('scan_errors.log', b'legacy-error\n'),
|
|
):
|
|
path = os.path.join(results_dir, name)
|
|
Path(path).write_bytes(payload)
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
db = migrate_runtime_safety.ScannerDB(
|
|
db_path=os.path.join(temp_dir, 'scanner.db'), db_url='', initialize=True,
|
|
)
|
|
try:
|
|
report = migrate_runtime_safety.initialize_projection_cursors_from_existing_files(
|
|
db, {'global': {'results_dir': results_dir}},
|
|
)
|
|
self.assertEqual(report, {
|
|
'scan_results': len(b'{"legacy":1}\n'),
|
|
'found_secrets': len(b'{"legacy":2}\n'),
|
|
'scan_errors': len(b'legacy-error\n'),
|
|
})
|
|
repeated = migrate_runtime_safety.initialize_projection_cursors_from_existing_files(
|
|
db, {'global': {'results_dir': results_dir}},
|
|
)
|
|
self.assertEqual(repeated, report)
|
|
finally:
|
|
db.close()
|
|
|
|
def test_docker_timeout_attempt_repair_is_fenced_and_preserves_results(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = migrate_runtime_safety.ScannerDB(
|
|
db_path=os.path.join(temp_dir, 'scanner.db'), db_url='', initialize=True,
|
|
)
|
|
now = '2026-09-01T00:00:00+00:00'
|
|
|
|
def add_queue(target, lease=False):
|
|
cursor = db.conn.execute(
|
|
'''INSERT INTO target_queue(
|
|
source, platform, target, normalized_target, status, attempts,
|
|
lease_owner, lease_token, available_after, created_at, updated_at
|
|
) VALUES ('dockerhub', 'docker', ?, ?, 'deferred', 0, ?, ?, ?, ?, ?)''',
|
|
(
|
|
target, target.lower(), 'worker' if lease else None,
|
|
'active-token' if lease else None, now, now, now,
|
|
),
|
|
)
|
|
return cursor.lastrowid
|
|
|
|
def add_scan(queue_id, target, metadata):
|
|
cursor = db.conn.execute(
|
|
'''INSERT INTO target_scans(
|
|
queue_id, source, target, normalized_target, scan_type, status,
|
|
error_count, created_at
|
|
) VALUES (?, 'dockerhub', ?, ?, 'docker', 'error', 1, ?)''',
|
|
(queue_id, target, target.lower(), now),
|
|
)
|
|
scan_id = cursor.lastrowid
|
|
payload = json.dumps(metadata, sort_keys=True, separators=(',', ':'))
|
|
encoded = payload.encode('ascii')
|
|
db.conn.execute(
|
|
'''INSERT INTO scan_result_compat(
|
|
target_scan_id, schema_version, metadata_json, metadata_sha256,
|
|
metadata_bytes, reconstruction_status, created_at
|
|
) VALUES (?, 2, ?, ?, ?, 'exact', ?)''',
|
|
(scan_id, payload, hashlib.sha256(encoded).hexdigest(), len(encoded), now),
|
|
)
|
|
return scan_id
|
|
|
|
exhausted_target = 'owner/exhausted@sha256:' + ('a' * 64)
|
|
ordinary_target = 'owner/ordinary@sha256:' + ('b' * 64)
|
|
active_target = 'owner/active@sha256:' + ('c' * 64)
|
|
exhausted_id = add_queue(exhausted_target)
|
|
ordinary_id = add_queue(ordinary_target)
|
|
active_id = add_queue(active_target, lease=True)
|
|
exhausted_scans = [
|
|
add_scan(exhausted_id, exhausted_target, {
|
|
'error_class': 'timeout', 'scan_meta': {'command_timed_out': True},
|
|
})
|
|
for _ in range(3)
|
|
]
|
|
add_scan(ordinary_id, ordinary_target, {'error_class': 'network'})
|
|
add_scan(active_id, active_target, {
|
|
'error_class': 'timeout', 'scan_meta': {'command_timed_out': True},
|
|
})
|
|
db.conn.execute(
|
|
'''INSERT INTO findings(
|
|
target_scan_id, source, target, detector_name, created_at
|
|
) VALUES (?, 'dockerhub', ?, 'FixtureDetector', ?)''',
|
|
(exhausted_scans[0], exhausted_target, now),
|
|
)
|
|
db.conn.commit()
|
|
try:
|
|
report = migrate_runtime_safety.repair_docker_timeout_attempts(
|
|
db, max_attempts=3, max_rows=100,
|
|
)
|
|
self.assertEqual(report, {
|
|
'examined': 2, 'repaired': 1, 'terminal': 1, 'unchanged': 1,
|
|
})
|
|
exhausted = db.conn.execute(
|
|
'SELECT status, attempts, available_after, last_error FROM target_queue WHERE id = ?',
|
|
(exhausted_id,),
|
|
).fetchone()
|
|
self.assertEqual((exhausted['status'], exhausted['attempts']), ('failed', 3))
|
|
self.assertIsNone(exhausted['available_after'])
|
|
self.assertEqual(
|
|
exhausted['last_error'],
|
|
'Docker timeout attempts exhausted by guarded repair',
|
|
)
|
|
ordinary = db.conn.execute(
|
|
'SELECT status, attempts FROM target_queue WHERE id = ?', (ordinary_id,),
|
|
).fetchone()
|
|
active = db.conn.execute(
|
|
'SELECT status, attempts, lease_token FROM target_queue WHERE id = ?',
|
|
(active_id,),
|
|
).fetchone()
|
|
self.assertEqual((ordinary['status'], ordinary['attempts']), ('deferred', 0))
|
|
self.assertEqual(
|
|
(active['status'], active['attempts'], active['lease_token']),
|
|
('deferred', 0, 'active-token'),
|
|
)
|
|
self.assertEqual(
|
|
db.conn.execute('SELECT COUNT(*) FROM target_scans').fetchone()[0], 5,
|
|
)
|
|
self.assertEqual(
|
|
db.conn.execute('SELECT COUNT(*) FROM scan_result_compat').fetchone()[0], 5,
|
|
)
|
|
self.assertEqual(
|
|
db.conn.execute('SELECT COUNT(*) FROM findings').fetchone()[0], 1,
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
def test_jsonl_issue_manifest_rejects_raw_payload_fields(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
path = os.path.join(temp_dir, 'review.json')
|
|
Path(path).write_text(json.dumps({
|
|
'schema': 1,
|
|
'type': 'truf-jsonl-projection-review',
|
|
'audit_sha256': 'a' * 64,
|
|
'issues': [{
|
|
'ledger_kind': 'scan_results',
|
|
'file': 'scan_results.000001.jsonl',
|
|
'offset': 1,
|
|
'length': 2,
|
|
'sha256': 'b' * 64,
|
|
'classification': 'invalid_json',
|
|
'safe_identity': False,
|
|
'raw_payload': 'forbidden',
|
|
}],
|
|
}), encoding='ascii')
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
with self.assertRaisesRegex(RuntimeError, 'entry is invalid'):
|
|
migrate_runtime_safety.load_jsonl_issue_review_manifest(path)
|
|
|
|
def test_jsonl_conflict_manifest_registers_exact_historical_variants(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
results_dir = os.path.join(temp_dir, 'results')
|
|
ensure_private_directory(results_dir, reject_reparse=True)
|
|
path = os.path.join(results_dir, 'scan_errors.log')
|
|
identity = '2026-05-23T23:20:51.522121'
|
|
first = f'{identity}\twhen\tgithub\t{{"error":"one"}}\n'.encode()
|
|
second = f'{identity}\twhen\tgithub\t{{"error":"two","code":2}}\n'.encode()
|
|
Path(path).write_bytes(first + second)
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
sys.path.insert(0, str(APP_DIR))
|
|
import scanner
|
|
variants = []
|
|
for offset, row, value in (
|
|
(0, first, {'error': 'one'}),
|
|
(len(first), second, {'error': 'two', 'code': 2}),
|
|
):
|
|
variants.append({
|
|
'ledger_kind': 'scan_errors',
|
|
'file': 'scan_errors.log',
|
|
'offset': offset,
|
|
'length': len(row),
|
|
'identity_sha256': hashlib.sha256(identity.encode()).hexdigest(),
|
|
'payload_sha256': hashlib.sha256(row).hexdigest(),
|
|
'field_name_set_sha256': scanner._projection_field_name_set_sha256(value),
|
|
'classification': 'historical_payload_variant',
|
|
'occurrences': 1,
|
|
})
|
|
manifest_path = os.path.join(temp_dir, 'conflict-review.json')
|
|
Path(manifest_path).write_text(json.dumps({
|
|
'schema': 1,
|
|
'type': 'truf-jsonl-projection-conflict-review',
|
|
'audit_sha256': hashlib.sha256(b'conflict-audit').hexdigest(),
|
|
'variants': variants,
|
|
}), encoding='ascii')
|
|
migrate_runtime_safety.harden_private_file(manifest_path)
|
|
config = {
|
|
'global': {
|
|
'root_dir': temp_dir,
|
|
'runtime_dir': os.path.join(temp_dir, 'runtime'),
|
|
'results_dir': results_dir,
|
|
},
|
|
'supervisor': {},
|
|
}
|
|
args = self.args(
|
|
reconcile_jsonl_projections=True,
|
|
resolve_jsonl_conflict_manifest=manifest_path,
|
|
until_complete=True,
|
|
max_rows=10,
|
|
max_bytes=1024 * 1024,
|
|
max_seconds=1,
|
|
max_batches=10,
|
|
)
|
|
with mock.patch.object(migrate_runtime_safety, 'parse_args', return_value=args), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_config', return_value=config), \
|
|
mock.patch.object(migrate_runtime_safety, 'load_postgres_environment'), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'database_url_from_env',
|
|
return_value='postgresql://truf:fixture@127.0.0.1:5432/truf',
|
|
), \
|
|
mock.patch.object(migrate_runtime_safety, 'require_runtime_hardening_stopped'):
|
|
self.assertEqual(migrate_runtime_safety.main(), 0)
|
|
ledger_path = os.path.splitext(path)[0] + '.publication-ledger.sqlite3'
|
|
with contextlib.closing(sqlite3.connect(ledger_path)) as ledger:
|
|
count = ledger.execute(
|
|
'''SELECT COUNT(*) FROM publication_identity_variant
|
|
WHERE identity_key = 'error_row_id' AND identity_value = ?''',
|
|
(identity,),
|
|
).fetchone()[0]
|
|
self.assertEqual(count, 2)
|
|
|
|
def test_jsonl_conflict_manifest_rejects_raw_payload_fields(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
path = os.path.join(temp_dir, 'conflicts.json')
|
|
Path(path).write_text(json.dumps({
|
|
'schema': 1,
|
|
'type': 'truf-jsonl-projection-conflict-review',
|
|
'audit_sha256': 'a' * 64,
|
|
'variants': [{
|
|
'ledger_kind': 'scan_errors',
|
|
'file': 'scan_errors.000001.log',
|
|
'offset': 1,
|
|
'length': 2,
|
|
'identity_sha256': 'b' * 64,
|
|
'payload_sha256': 'c' * 64,
|
|
'field_name_set_sha256': 'd' * 64,
|
|
'classification': 'historical_payload_variant',
|
|
'occurrences': 1,
|
|
'raw_payload': 'forbidden',
|
|
}],
|
|
}), encoding='ascii')
|
|
migrate_runtime_safety.harden_private_file(path)
|
|
with self.assertRaisesRegex(RuntimeError, 'entry is invalid'):
|
|
migrate_runtime_safety.load_jsonl_conflict_review_manifest(path)
|
|
|
|
@unittest.skipUnless(os.name == 'nt', 'legacy Windows private-native hardening policy')
|
|
def test_offline_hardening_creates_full_layout_and_hardens_required_files(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
ensure_private_directory(temp_dir, reject_reparse=True)
|
|
project = os.path.join(temp_dir, 'app')
|
|
os.makedirs(project)
|
|
config_path = os.path.join(project, 'config.yaml')
|
|
secrets_path = os.path.join(project, 'secrets.yaml')
|
|
env_path = os.path.join(temp_dir, '.env.postgres')
|
|
provider_dir = os.path.join(project, 'keycheckers', 'fixture')
|
|
os.makedirs(provider_dir)
|
|
provider_path = os.path.join(provider_dir, 'fixtureKeycheck.py')
|
|
runtime = os.path.join(temp_dir, 'runtime')
|
|
os.makedirs(runtime)
|
|
git_dir = os.path.join(runtime, 'git', 'cmd')
|
|
os.makedirs(git_dir)
|
|
git_path = os.path.join(git_dir, 'git.exe')
|
|
external_postgres_data = os.path.join(temp_dir, 'external-postgres-data')
|
|
openrouter_path = os.path.join(runtime, 'check-openrouter-keys.ps1')
|
|
start_launcher = os.path.join(temp_dir, 'start_runtime.ps1')
|
|
stop_launcher = os.path.join(temp_dir, 'stop_runtime.ps1')
|
|
trufflehog_path = os.path.join(temp_dir, 'trufflehog.exe')
|
|
for path, content in (
|
|
(config_path, 'global: {}\n'),
|
|
(secrets_path, 'auth_pools: {}\n'),
|
|
(env_path, 'TRUF_POSTGRES_PASSWORD=fixture\n'),
|
|
(provider_path, 'pass\n'),
|
|
(openrouter_path, '# fixture\n'),
|
|
(start_launcher, '# start fixture\n'),
|
|
(stop_launcher, '# stop fixture\n'),
|
|
(trufflehog_path, 'fixture executable\n'),
|
|
(git_path, 'fixture Git executable\n'),
|
|
):
|
|
Path(path).write_text(content, encoding='ascii')
|
|
global_config = {
|
|
'root_dir': temp_dir,
|
|
'project_dir': project,
|
|
'runtime_dir': runtime,
|
|
'results_dir': os.path.join(runtime, 'results'),
|
|
'result_spool_dir': os.path.join(runtime, 'result_spool'),
|
|
'control_dir': os.path.join(runtime, 'control'),
|
|
'queue_dir': os.path.join(runtime, 'queues'),
|
|
'state_dir': os.path.join(runtime, 'state'),
|
|
'keycheck_dir': os.path.join(runtime, 'keychecks'),
|
|
'postman_cache_dir': os.path.join(runtime, 'postman_cache'),
|
|
'gharchive_cache_dir': os.path.join(runtime, 'state', 'gharchive_cache'),
|
|
'log_dir': os.path.join(runtime, 'logs'),
|
|
'work_dir': os.path.join(temp_dir, 'work'),
|
|
'postgres_data_dir': external_postgres_data,
|
|
'secrets_file': secrets_path,
|
|
'trufflehog_path': trufflehog_path,
|
|
}
|
|
config = {
|
|
'global': global_config,
|
|
'supervisor': {
|
|
'control_dir': global_config['control_dir'],
|
|
'log_dir': global_config['log_dir'],
|
|
'state_dir': global_config['state_dir'],
|
|
},
|
|
}
|
|
with mock.patch.object(migrate_runtime_safety, 'harden_private_tree', return_value=0), \
|
|
mock.patch.object(
|
|
migrate_runtime_safety,
|
|
'harden_private_file',
|
|
wraps=migrate_runtime_safety.harden_private_file,
|
|
) as harden_file:
|
|
migrate_runtime_safety.harden_runtime_paths(
|
|
config,
|
|
env_path=env_path,
|
|
config_path=config_path,
|
|
)
|
|
hardened_files = {
|
|
os.path.normcase(os.path.realpath(os.path.abspath(call.args[0])))
|
|
for call in harden_file.call_args_list
|
|
}
|
|
self.assertTrue({
|
|
os.path.normcase(os.path.realpath(start_launcher)),
|
|
os.path.normcase(os.path.realpath(stop_launcher)),
|
|
os.path.normcase(os.path.realpath(git_path)),
|
|
}.issubset(hardened_files))
|
|
required_dirs = [
|
|
global_config[key]
|
|
for key in (
|
|
'root_dir', 'project_dir', 'runtime_dir', 'results_dir',
|
|
'result_spool_dir', 'control_dir', 'queue_dir', 'state_dir',
|
|
'keycheck_dir', 'postman_cache_dir', 'gharchive_cache_dir',
|
|
'log_dir', 'work_dir',
|
|
)
|
|
]
|
|
required_dirs.append(os.path.join(runtime, 'postgres'))
|
|
required_dirs.append(external_postgres_data)
|
|
self.assertTrue(all(private_directory_ready(path) for path in required_dirs))
|
|
self.assertTrue(all(private_file_ready(path) for path in (
|
|
config_path, secrets_path, env_path, provider_path, openrouter_path,
|
|
start_launcher, stop_launcher, trufflehog_path, git_path,
|
|
)))
|
|
self.assertTrue(preflight_lifecycle_paths(config_path, config))
|
|
|
|
def test_legacy_postgres_import_tool_is_retired_before_opening_any_database(self):
|
|
with mock.patch.object(migrate_observability_db, 'sqlite_connect_ro') as sqlite_connect, \
|
|
mock.patch.object(migrate_observability_db, 'ScannerDB') as scanner_db:
|
|
with self.assertRaisesRegex(SystemExit, 'retired'):
|
|
migrate_observability_db.main()
|
|
sqlite_connect.assert_not_called()
|
|
scanner_db.assert_not_called()
|
|
|
|
def test_active_database_session_or_supervisor_metadata_refuses_migration(self):
|
|
class Connection:
|
|
def execute(self, sql, params=None):
|
|
return RowCursor(rows=[{'pid': 9, 'application_name': 'scanner', 'state': 'idle'}])
|
|
|
|
def commit(self):
|
|
pass
|
|
|
|
with self.assertRaisesRegex(RuntimeError, 'application session'):
|
|
migrate_runtime_safety._require_no_postgres_application_sessions(SimpleNamespace(conn=Connection()))
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
instance = os.path.join(temp_dir, 'supervisor.instance.json')
|
|
Path(instance).write_text('{}', encoding='ascii')
|
|
config = {
|
|
'global': {'state_dir': os.path.join(temp_dir, 'state')},
|
|
'supervisor': {'instance_file': instance, 'log_dir': os.path.join(temp_dir, 'logs')},
|
|
}
|
|
with self.assertRaisesRegex(RuntimeError, 'supervisor metadata'):
|
|
migrate_runtime_safety.require_local_sources_stopped(config)
|
|
|
|
def test_scheduled_keycheck_module_contains_no_schema_ddl(self):
|
|
source = (APP_DIR / 'keycheck_runner.py').read_text(encoding='utf-8')
|
|
for token in ('ALTER TABLE', 'CREATE TABLE', 'CREATE INDEX', 'CREATE UNIQUE INDEX'):
|
|
self.assertNotIn(token, source)
|
|
|
|
def test_maintenance_uses_only_the_shared_cluster_authority_lock(self):
|
|
source = (APP_DIR / 'migrate_runtime_safety.py').read_text(encoding='utf-8')
|
|
self.assertNotIn('MigrationLocalLock', source)
|
|
self.assertEqual(source.count('with ClusterAuthorityLock('), 12)
|
|
|
|
def test_docker_compose_postgres_is_fail_closed_and_noncanonical(self):
|
|
compose = (ROOT / 'docker-compose.postgres.yml').read_text(encoding='utf-8')
|
|
self.assertIn('noncanonical-manual-recovery', compose)
|
|
self.assertIn('restart: "no"', compose)
|
|
self.assertIn('TRUF_DOCKER_POSTGRES_PORT:?', compose)
|
|
self.assertIn('D:/truf/runtime/postgres-docker-noncanonical', compose)
|
|
self.assertNotIn('restart: unless-stopped', compose)
|
|
self.assertNotIn('truf_postgres_data:', compose)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|