Initial server source import
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
import concurrent.futures
|
||||
import os
|
||||
from pathlib import Path
|
||||
import sqlite3
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from unittest import mock
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
APP_DIR = ROOT / 'app'
|
||||
sys.path.insert(0, str(APP_DIR))
|
||||
|
||||
import scanner
|
||||
import janitor
|
||||
|
||||
|
||||
class RecordingLease:
|
||||
def __init__(self):
|
||||
self.releasable = True
|
||||
self.children = []
|
||||
self.release_calls = 0
|
||||
self.non_release_calls = 0
|
||||
|
||||
def set_child_pid(self, pid):
|
||||
self.children.append(pid)
|
||||
return True
|
||||
|
||||
def mark_non_releasable(self):
|
||||
self.non_release_calls += 1
|
||||
self.releasable = False
|
||||
|
||||
def release(self):
|
||||
self.release_calls += 1
|
||||
|
||||
|
||||
class CompleteProcess:
|
||||
returncode = 0
|
||||
job_membership_verified = True
|
||||
|
||||
def __init__(self, pid):
|
||||
self.pid = pid
|
||||
self.payload_identity = {
|
||||
'pid': pid,
|
||||
'creation_time': f'fixture-{pid}',
|
||||
'executable': sys.executable,
|
||||
}
|
||||
|
||||
def poll(self):
|
||||
return 0
|
||||
|
||||
|
||||
def private_work_dir(temp_dir):
|
||||
work_dir = os.path.join(temp_dir, 'work')
|
||||
scanner.ensure_private_directory(work_dir, reject_reparse=True)
|
||||
return work_dir
|
||||
|
||||
|
||||
class ArtifactLifecycleSlotTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
|
||||
|
||||
def tearDown(self):
|
||||
if hasattr(scanner._scan_slot_scope_local, 'scope'):
|
||||
del scanner._scan_slot_scope_local.scope
|
||||
with scanner._scan_slot_fatal_lock:
|
||||
scanner._scan_slot_fatal_detail = None
|
||||
scanner._scan_slot_fatal_event.clear()
|
||||
|
||||
def test_existing_limiter_schema_gains_slot_kind_and_single_bonus_index(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
connection = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
connection.executescript('''
|
||||
CREATE TABLE scan_slots (
|
||||
slot_id TEXT PRIMARY KEY,
|
||||
owner_pid INTEGER NOT NULL,
|
||||
owner_thread INTEGER NOT NULL,
|
||||
owner_source TEXT,
|
||||
owner_creation_time TEXT,
|
||||
owner_executable TEXT,
|
||||
child_pid INTEGER,
|
||||
child_creation_time TEXT,
|
||||
child_executable TEXT,
|
||||
command TEXT,
|
||||
acquired_at REAL NOT NULL,
|
||||
updated_at REAL NOT NULL
|
||||
);
|
||||
''')
|
||||
connection.commit()
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
scanner.ensure_scan_limiter_db(limiter_db)
|
||||
connection = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
columns = {
|
||||
row[1] for row in connection.execute('PRAGMA table_info(scan_slots)')
|
||||
}
|
||||
indexes = {
|
||||
row[1] for row in connection.execute('PRAGMA index_list(scan_slots)')
|
||||
}
|
||||
finally:
|
||||
connection.close()
|
||||
self.assertIn('slot_kind', columns)
|
||||
self.assertIn('idx_scan_slots_single_bonus', indexes)
|
||||
|
||||
def test_two_workers_cannot_enter_target_staging_concurrently(self):
|
||||
first_entered = threading.Event()
|
||||
second_entered = threading.Event()
|
||||
release_first = threading.Event()
|
||||
state_lock = threading.Lock()
|
||||
state = {'active': 0, 'maximum': 0, 'entries': 0}
|
||||
|
||||
def stage_target(target, provider=None, **kwargs):
|
||||
with state_lock:
|
||||
state['active'] += 1
|
||||
state['maximum'] = max(state['maximum'], state['active'])
|
||||
state['entries'] += 1
|
||||
entry = state['entries']
|
||||
try:
|
||||
if entry == 1:
|
||||
first_entered.set()
|
||||
if not release_first.wait(5):
|
||||
raise RuntimeError('test did not release first target')
|
||||
else:
|
||||
second_entered.set()
|
||||
return {'findings': [], 'errors': []}
|
||||
finally:
|
||||
with state_lock:
|
||||
state['active'] -= 1
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
with mock.patch.object(scanner.scan_config, 'max_active_scans', 1), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01), \
|
||||
mock.patch.object(scanner, 'scan_git_repo', side_effect=stage_target):
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as caller:
|
||||
future = caller.submit(
|
||||
scanner.scan_targets_batch,
|
||||
['first', 'second'],
|
||||
'git',
|
||||
max_workers=2,
|
||||
persist_results=False,
|
||||
)
|
||||
try:
|
||||
self.assertTrue(first_entered.wait(3))
|
||||
self.assertFalse(second_entered.wait(0.3))
|
||||
finally:
|
||||
release_first.set()
|
||||
results = future.result(timeout=5)
|
||||
|
||||
self.assertTrue(second_entered.is_set())
|
||||
self.assertEqual(state['maximum'], 1)
|
||||
self.assertEqual(len(results), 2)
|
||||
conn = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
self.assertEqual(conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0], 0)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_fair_waiters_rotate_to_the_source_least_recently_granted(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(
|
||||
os.environ, {'SCANNER_SOURCE': ''}, clear=False,
|
||||
):
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
order = []
|
||||
order_lock = threading.Lock()
|
||||
acquired = {'a': threading.Event(), 'b': threading.Event()}
|
||||
release = {'a': threading.Event(), 'b': threading.Event()}
|
||||
|
||||
def waiter(source):
|
||||
lease = scanner.acquire_scan_slot(['dispatch', source], 60)
|
||||
with order_lock:
|
||||
order.append(source)
|
||||
acquired[source].set()
|
||||
release[source].wait(5)
|
||||
lease.release()
|
||||
|
||||
with mock.patch.object(scanner.scan_config, 'max_active_scans', 1), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01):
|
||||
occupant = scanner.acquire_scan_slot(['dispatch', 'a'], 60)
|
||||
threads = [
|
||||
threading.Thread(target=waiter, args=('a',)),
|
||||
threading.Thread(target=waiter, args=('b',)),
|
||||
]
|
||||
for thread in threads:
|
||||
thread.start()
|
||||
time.sleep(0.1)
|
||||
occupant.release()
|
||||
self.assertTrue(acquired['b'].wait(3))
|
||||
self.assertFalse(acquired['a'].is_set())
|
||||
release['b'].set()
|
||||
self.assertTrue(acquired['a'].wait(3))
|
||||
release['a'].set()
|
||||
for thread in threads:
|
||||
thread.join(5)
|
||||
self.assertEqual(order, ['b', 'a'])
|
||||
|
||||
def test_three_base_slots_remain_guaranteed_with_one_gated_bonus(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(
|
||||
os.environ, {'SCANNER_SOURCE': ''}, clear=False,
|
||||
):
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
policy = (
|
||||
mock.patch.object(scanner.scan_config, 'max_active_scans', 3),
|
||||
mock.patch.object(scanner.scan_config, 'opportunistic_scan_slots', 1),
|
||||
mock.patch.object(scanner.scan_config, 'opportunistic_scan_sources', ['github']),
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db),
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01),
|
||||
mock.patch.object(scanner, 'opportunistic_scan_slot_allowed', return_value=True),
|
||||
)
|
||||
with policy[0], policy[1], policy[2], policy[3], policy[4], policy[5]:
|
||||
base = [
|
||||
scanner.acquire_scan_slot(['dispatch', 'github'], 60, wait=False)
|
||||
for _ in range(3)
|
||||
]
|
||||
bonus = scanner.acquire_scan_slot(['dispatch', 'github'], 60, wait=False)
|
||||
fifth = scanner.acquire_scan_slot(['dispatch', 'github'], 60, wait=False)
|
||||
self.assertTrue(all(base))
|
||||
self.assertIsNotNone(bonus)
|
||||
self.assertIsNone(fifth)
|
||||
|
||||
connection = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
self.assertEqual(
|
||||
dict(connection.execute(
|
||||
'SELECT slot_kind, COUNT(*) FROM scan_slots GROUP BY slot_kind'
|
||||
)),
|
||||
{'base': 3, 'bonus': 1},
|
||||
)
|
||||
finally:
|
||||
connection.close()
|
||||
|
||||
base[0].release()
|
||||
replacement = scanner.acquire_scan_slot(
|
||||
['dispatch', 'github'], 60, wait=False,
|
||||
)
|
||||
self.assertIsNotNone(replacement)
|
||||
connection = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
kinds = dict(connection.execute(
|
||||
'SELECT slot_kind, COUNT(*) FROM scan_slots GROUP BY slot_kind'
|
||||
))
|
||||
finally:
|
||||
connection.close()
|
||||
self.assertEqual(kinds, {'base': 3, 'bonus': 1})
|
||||
for lease in base[1:] + [bonus, replacement]:
|
||||
lease.release()
|
||||
|
||||
def test_bonus_slot_is_denied_to_docker_and_on_probe_failure(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(
|
||||
os.environ, {'SCANNER_SOURCE': ''}, clear=False,
|
||||
):
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
with mock.patch.object(scanner.scan_config, 'max_active_scans', 3), \
|
||||
mock.patch.object(scanner.scan_config, 'opportunistic_scan_slots', 1), \
|
||||
mock.patch.object(scanner.scan_config, 'opportunistic_scan_sources', ['github']), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01), \
|
||||
mock.patch.object(scanner, 'windows_scan_capacity_snapshot', side_effect=OSError('probe')):
|
||||
base = [
|
||||
scanner.acquire_scan_slot(['dispatch', 'github'], 60, wait=False)
|
||||
for _ in range(3)
|
||||
]
|
||||
self.assertIsNone(scanner.acquire_scan_slot(
|
||||
['dispatch', 'dockerhub'], 60, wait=False,
|
||||
))
|
||||
self.assertIsNone(scanner.acquire_scan_slot(
|
||||
['dispatch', 'github'], 60, wait=False,
|
||||
))
|
||||
for lease in base:
|
||||
lease.release()
|
||||
|
||||
def test_preacquired_three_slots_release_before_blocked_downstream_work(self):
|
||||
stage_barrier = threading.Barrier(3)
|
||||
downstream_entered = threading.Event()
|
||||
release_downstream = threading.Event()
|
||||
state_lock = threading.Lock()
|
||||
state = {'active': 0, 'maximum': 0, 'sink_calls': 0}
|
||||
|
||||
def stage_target(target, provider=None, **kwargs):
|
||||
with state_lock:
|
||||
state['active'] += 1
|
||||
state['maximum'] = max(state['maximum'], state['active'])
|
||||
try:
|
||||
stage_barrier.wait(timeout=5)
|
||||
return {'findings': [], 'errors': []}
|
||||
finally:
|
||||
with state_lock:
|
||||
state['active'] -= 1
|
||||
|
||||
def sink(_result):
|
||||
with state_lock:
|
||||
state['sink_calls'] += 1
|
||||
|
||||
def blocked_downstream(completed, total, target):
|
||||
if target not in ('Scan completed', 'Scan completed with persistence failures') and not downstream_entered.is_set():
|
||||
downstream_entered.set()
|
||||
if not release_downstream.wait(5):
|
||||
raise RuntimeError('test did not release downstream work')
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
with mock.patch.object(scanner.scan_config, 'max_active_scans', 3), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01), \
|
||||
mock.patch.object(scanner, 'scan_git_repo', side_effect=stage_target):
|
||||
leases = scanner.acquire_scan_slot_leases(['dispatch', 'git'], 3, timeout_sec=60)
|
||||
self.assertEqual(len(leases), 3)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as caller:
|
||||
future = caller.submit(
|
||||
scanner.scan_targets_batch,
|
||||
['one', 'two', 'three'],
|
||||
'git',
|
||||
max_workers=3,
|
||||
progress_callback=blocked_downstream,
|
||||
persist_results=False,
|
||||
result_sink=sink,
|
||||
scan_slot_leases=leases,
|
||||
sink_within_scan_slot=True,
|
||||
)
|
||||
self.assertTrue(downstream_entered.wait(5))
|
||||
deadline = time.monotonic() + 3
|
||||
active = None
|
||||
while time.monotonic() < deadline:
|
||||
conn = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
active = conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
if active == 0:
|
||||
break
|
||||
time.sleep(0.02)
|
||||
self.assertEqual(active, 0)
|
||||
release_downstream.set()
|
||||
results = future.result(timeout=10)
|
||||
|
||||
self.assertEqual(state['maximum'], 3)
|
||||
self.assertEqual(state['sink_calls'], 3)
|
||||
self.assertEqual(len(results), 3)
|
||||
conn = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
self.assertEqual(conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0], 0)
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
def test_durable_bundle_handoff_releases_all_permits_before_db_notification(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
limiter_db = os.path.join(temp_dir, 'scan-limiter.sqlite')
|
||||
bundle_root = os.path.join(temp_dir, 'bundles')
|
||||
scanner.ensure_private_directory(bundle_root, reject_reparse=True)
|
||||
notification_lock = threading.Lock()
|
||||
notification_state = {'entered': 0}
|
||||
all_notifications_entered = threading.Event()
|
||||
release_notifications = threading.Event()
|
||||
|
||||
def stage(index, lease):
|
||||
event_id = f'{index + 1:032x}'
|
||||
bundle_id = f'{index + 100:032x}'
|
||||
reservation = {
|
||||
'reservation_id': index + 1,
|
||||
'reservation_token': f'reservation-{index}',
|
||||
'bundle_id': bundle_id,
|
||||
'scan_event_id': event_id,
|
||||
'queue_id': index + 1,
|
||||
'claim_lease_token': f'lease-{index}',
|
||||
'declared_bundle_bytes': 1024 * 1024,
|
||||
'ready_relative_path': f'ready/{bundle_id[:2]}/{bundle_id}.trb',
|
||||
'source': 'fixture',
|
||||
'platform': 'git',
|
||||
'target': f'target-{index}',
|
||||
}
|
||||
with scanner.scan_slot_scope(['scan-target', 'git'], 60, lease=lease):
|
||||
result = {
|
||||
'scan_event_id': event_id,
|
||||
'target': f'target-{index}',
|
||||
'scan_type': 'git',
|
||||
'timestamp': '2026-07-27T00:00:00+00:00',
|
||||
'findings': [],
|
||||
'errors': [],
|
||||
}
|
||||
scanner.stage_result_bundle(
|
||||
result, reservation, bundle_root, {}, {'queue_status': 'done'},
|
||||
)
|
||||
with notification_lock:
|
||||
notification_state['entered'] += 1
|
||||
if notification_state['entered'] == 3:
|
||||
all_notifications_entered.set()
|
||||
release_notifications.wait(5)
|
||||
|
||||
with mock.patch.object(scanner.scan_config, 'max_active_scans', 3), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_db), \
|
||||
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01):
|
||||
leases = scanner.acquire_scan_slot_leases(['dispatch', 'git'], 3, timeout_sec=60)
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=3) as executor:
|
||||
futures = [executor.submit(stage, index, lease) for index, lease in enumerate(leases)]
|
||||
deadline = time.monotonic() + 5
|
||||
active = None
|
||||
while time.monotonic() < deadline:
|
||||
conn = sqlite3.connect(limiter_db)
|
||||
try:
|
||||
active = conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0]
|
||||
finally:
|
||||
conn.close()
|
||||
if active == 0 and all_notifications_entered.is_set():
|
||||
break
|
||||
time.sleep(0.02)
|
||||
self.assertEqual(active, 0)
|
||||
self.assertTrue(all_notifications_entered.is_set())
|
||||
release_notifications.set()
|
||||
for future in futures:
|
||||
future.result(timeout=5)
|
||||
|
||||
def test_nested_commands_borrow_one_outer_physical_lease(self):
|
||||
lease = RecordingLease()
|
||||
processes = [CompleteProcess(4101), CompleteProcess(4102)]
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
work_dir = private_work_dir(temp_dir)
|
||||
|
||||
def scan_target(target, provider=None, **kwargs):
|
||||
first = scanner.run_command(['fixture', 'one'], timeout_sec=5)
|
||||
second = scanner.run_command(['fixture', 'two'], timeout_sec=5)
|
||||
self.assertEqual(first, ('', '', 0))
|
||||
self.assertEqual(second, ('', '', 0))
|
||||
return {'findings': [], 'errors': []}
|
||||
|
||||
with mock.patch.object(scanner, 'acquire_scan_slot', return_value=lease) as acquire, \
|
||||
mock.patch.object(scanner, 'scan_git_repo', side_effect=scan_target), \
|
||||
mock.patch.object(scanner.scan_config, 'work_dir', work_dir), \
|
||||
mock.patch.object(scanner, 'require_trufflehog_launch_authority'), \
|
||||
mock.patch.object(scanner, 'OwnedProcess', side_effect=processes), \
|
||||
mock.patch.object(scanner.scan_config, 'min_free_gb', 0), \
|
||||
mock.patch.object(scanner.scan_config, 'trufflehog_job_memory_limit_bytes', 4 * 1024 * 1024 * 1024):
|
||||
results = scanner.scan_targets_batch(
|
||||
['target'], 'git', max_workers=1, persist_results=False,
|
||||
)
|
||||
|
||||
self.assertEqual(results[0]['errors'], [])
|
||||
self.assertEqual(acquire.call_count, 1)
|
||||
self.assertEqual(lease.children, [4101, 4102])
|
||||
self.assertEqual(lease.release_calls, 1)
|
||||
|
||||
def test_scope_clears_after_exception_before_executor_thread_reuse(self):
|
||||
lease = RecordingLease()
|
||||
worker_threads = []
|
||||
|
||||
def fail_in_scope():
|
||||
worker_threads.append(threading.get_ident())
|
||||
with scanner.scan_slot_scope(['scan-target', 'fixture']):
|
||||
raise RuntimeError('fixture failure')
|
||||
|
||||
def inspect_reused_thread():
|
||||
worker_threads.append(threading.get_ident())
|
||||
return scanner.scoped_scan_slot_lease()
|
||||
|
||||
with mock.patch.object(scanner, 'acquire_scan_slot', return_value=lease) as acquire:
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
with self.assertRaisesRegex(RuntimeError, 'fixture failure'):
|
||||
executor.submit(fail_in_scope).result(timeout=3)
|
||||
borrowed, current = executor.submit(inspect_reused_thread).result(timeout=3)
|
||||
|
||||
self.assertEqual(worker_threads[0], worker_threads[1])
|
||||
self.assertFalse(borrowed)
|
||||
self.assertIsNone(current)
|
||||
self.assertEqual(acquire.call_count, 1)
|
||||
self.assertEqual(lease.release_calls, 1)
|
||||
|
||||
def test_standalone_command_acquires_and_releases_its_own_lease(self):
|
||||
lease = RecordingLease()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
work_dir = private_work_dir(temp_dir)
|
||||
with mock.patch.object(scanner, 'acquire_scan_slot', return_value=lease) as acquire, \
|
||||
mock.patch.object(scanner.scan_config, 'work_dir', work_dir), \
|
||||
mock.patch.object(scanner, 'require_trufflehog_launch_authority'), \
|
||||
mock.patch.object(scanner, 'OwnedProcess', return_value=CompleteProcess(4151)), \
|
||||
mock.patch.object(scanner.scan_config, 'min_free_gb', 0), \
|
||||
mock.patch.object(scanner.scan_config, 'trufflehog_job_memory_limit_bytes', 4 * 1024 * 1024 * 1024):
|
||||
result = scanner.run_command(['fixture'], timeout_sec=5)
|
||||
|
||||
self.assertEqual(result, ('', '', 0))
|
||||
self.assertEqual(acquire.call_count, 1)
|
||||
self.assertEqual(lease.children, [4151])
|
||||
self.assertEqual(lease.release_calls, 1)
|
||||
|
||||
def test_uncertain_nested_child_termination_prevents_outer_release(self):
|
||||
class UnstoppableProcess:
|
||||
pid = 4201
|
||||
returncode = None
|
||||
job_membership_verified = True
|
||||
payload_identity = {
|
||||
'pid': pid,
|
||||
'creation_time': 'fixture-4201',
|
||||
'executable': sys.executable,
|
||||
}
|
||||
|
||||
def poll(self):
|
||||
return None
|
||||
|
||||
def kill(self):
|
||||
return None
|
||||
|
||||
def wait(self, timeout=None):
|
||||
raise subprocess.TimeoutExpired(['fixture'], timeout)
|
||||
|
||||
lease = RecordingLease()
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
work_dir = private_work_dir(temp_dir)
|
||||
with mock.patch.object(scanner.scan_config, 'work_dir', work_dir), \
|
||||
mock.patch.object(scanner.scan_config, 'min_free_gb', 0):
|
||||
command_work_dir = scanner.create_command_work_dir()
|
||||
with mock.patch.object(scanner, 'acquire_scan_slot', return_value=lease) as acquire, \
|
||||
mock.patch.object(scanner.scan_config, 'work_dir', work_dir), \
|
||||
mock.patch.object(scanner, 'create_command_work_dir', return_value=command_work_dir), \
|
||||
mock.patch.object(scanner, 'require_trufflehog_launch_authority'), \
|
||||
mock.patch.object(scanner, 'OwnedProcess', return_value=UnstoppableProcess()), \
|
||||
mock.patch.object(scanner.shutil, 'disk_usage', return_value=mock.Mock(free=1)), \
|
||||
mock.patch.object(scanner.scan_config, 'min_free_gb', 1), \
|
||||
mock.patch.object(scanner.scan_config, 'trufflehog_job_memory_limit_bytes', 4 * 1024 * 1024 * 1024):
|
||||
with scanner.scan_slot_scope(['scan-target', 'fixture']):
|
||||
with self.assertRaisesRegex(scanner.ScanSlotFatalError, 'termination was not confirmed'):
|
||||
scanner.run_command(['fixture'], timeout_sec=5)
|
||||
self.assertEqual(acquire.call_count, 1)
|
||||
self.assertFalse(lease.releasable)
|
||||
self.assertGreaterEqual(lease.non_release_calls, 1)
|
||||
self.assertEqual(lease.release_calls, 0)
|
||||
self.assertEqual(scanner.scoped_scan_slot_lease(), (False, None))
|
||||
|
||||
def test_command_workdir_marker_is_schema_two_and_only_janitor_removes_stale_tree(self):
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
work_dir = os.path.join(temp_dir, 'work')
|
||||
scanner.ensure_private_directory(work_dir, reject_reparse=True)
|
||||
markerless = os.path.join(work_dir, 'trufflehog-run-foreign')
|
||||
scanner.ensure_private_directory(markerless, reject_reparse=True)
|
||||
|
||||
with mock.patch.object(scanner.scan_config, 'work_dir', work_dir), \
|
||||
mock.patch.object(scanner.scan_config, 'min_free_gb', 0):
|
||||
command_dir = scanner.create_command_work_dir()
|
||||
marker = os.path.join(command_dir, scanner.TEMP_OWNER_FILE)
|
||||
owner = scanner.read_temp_owner(command_dir)
|
||||
self.assertEqual(owner.get('owner_pid'), os.getpid())
|
||||
self.assertEqual(owner.get('schema'), 2)
|
||||
self.assertTrue(owner.get('owner_creation_time'))
|
||||
self.assertTrue(owner.get('owner_executable'))
|
||||
self.assertTrue(scanner.private_file_ready(marker))
|
||||
|
||||
old = time.time() - 7200
|
||||
os.utime(command_dir, (old, old))
|
||||
os.utime(markerless, (old, old))
|
||||
removed = scanner.cleanup_stale_temp_dirs(age_minutes=1, log=False)
|
||||
self.assertEqual(removed, 0)
|
||||
with mock.patch.object(janitor, 'exact_process_identity_state', return_value='dead'):
|
||||
report = janitor.run_janitor_pass(
|
||||
work_dir,
|
||||
[owner['owner_executable'], owner['parent_executable']],
|
||||
minimum_age_sec=0,
|
||||
)
|
||||
self.assertEqual(report['removed'], 1)
|
||||
self.assertFalse(os.path.exists(command_dir))
|
||||
self.assertTrue(os.path.isdir(markerless))
|
||||
|
||||
before = set(os.listdir(work_dir))
|
||||
with mock.patch.object(scanner, 'atomic_write_private_json', side_effect=OSError('marker denied')):
|
||||
with self.assertRaisesRegex(RuntimeError, 'required temp owner marker'):
|
||||
scanner.create_command_work_dir()
|
||||
self.assertEqual(set(os.listdir(work_dir)), before)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user