Files
truf-server/tests/test_package_postman_harvest_limits.py
2026-09-30 20:30:56 +03:00

246 lines
12 KiB
Python

import json
import os
from pathlib import Path
import sys
import tempfile
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 console_runner
import scanner
class PackagePostmanHarvestLimitTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
def _cache_patches(self, runtime_dir, cache_dir):
return (
mock.patch.object(scanner.scan_config, 'runtime_dir', runtime_dir),
mock.patch.object(scanner.scan_config, 'postman_cache_dir', cache_dir),
mock.patch.object(scanner.scan_config, 'postman_cache_max_items', 10000),
mock.patch.object(scanner.scan_config, 'postman_cache_max_bytes', 64 * 1024 * 1024),
mock.patch.object(scanner.scan_config, 'postman_cache_min_free_bytes', 0),
)
def test_thousands_of_matches_stop_at_artifact_cap_and_scan_cache_once(self):
with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, 'extract')
cache_dir = os.path.join(temp_dir, 'cache')
os.mkdir(extract_dir)
scanner.ensure_private_directory(cache_dir, reject_reparse=True)
for index in range(2000):
Path(extract_dir, f'{index:04d}.postman_collection.json').write_text(
json.dumps({'token': f'value-{index:04d}'}),
encoding='utf-8',
)
warnings = []
patches = self._cache_patches(temp_dir, cache_dir)
with patches[0], patches[1], patches[2], patches[3], patches[4], \
mock.patch.object(scanner, '_prepare_postman_cache_entry', wraps=scanner._prepare_postman_cache_entry) as prepare, \
mock.patch.object(scanner, 'postman_cache_usage', wraps=scanner.postman_cache_usage) as usage:
started = time.perf_counter()
targets = scanner.find_postman_artifacts(
extract_dir,
'npm',
{'name': 'fixture', 'version': '1.0.0', 'tarball': 'https://example.invalid/package.tgz'},
cache_dir=cache_dir,
max_artifact_size_mb=1,
warnings=warnings,
max_artifacts=7,
max_total_bytes=1024 * 1024,
max_elapsed_sec=10,
)
elapsed = time.perf_counter() - started
self.assertEqual(len(targets), 7)
self.assertEqual(prepare.call_count, 7)
self.assertEqual(usage.call_count, 1)
self.assertEqual(scanner.postman_cache_usage(cache_dir)['items'], 7)
self.assertLess(elapsed, 5)
self.assertTrue(any('artifact limit of 7' in warning for warning in warnings))
self.assertLessEqual(len(warnings), scanner.POSTMAN_HARVEST_MAX_WARNINGS)
self.assertTrue(all(len(warning) <= scanner.POSTMAN_HARVEST_WARNING_MAX_CHARS for warning in warnings))
def test_aggregate_byte_cap_stops_before_reading_or_publishing_excess(self):
with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, 'extract')
cache_dir = os.path.join(temp_dir, 'cache')
os.mkdir(extract_dir)
scanner.ensure_private_directory(cache_dir, reject_reparse=True)
payloads = []
for index in range(6):
payload = json.dumps({'value': f'{index:04d}-fixed-width'}).encode('ascii')
payloads.append(payload)
Path(extract_dir, f'{index}.postman_environment.json').write_bytes(payload)
self.assertEqual(len({len(payload) for payload in payloads}), 1)
warnings = []
byte_limit = len(payloads[0]) * 2
patches = self._cache_patches(temp_dir, cache_dir)
with patches[0], patches[1], patches[2], patches[3], patches[4], \
mock.patch.object(scanner, '_prepare_postman_cache_entry', wraps=scanner._prepare_postman_cache_entry) as prepare, \
mock.patch.object(scanner, 'postman_cache_usage', wraps=scanner.postman_cache_usage) as usage:
targets = scanner.find_postman_artifacts(
extract_dir,
'pypi',
{'name': 'fixture', 'version': '2.0.0', 'artifact': 'https://example.invalid/package.whl'},
cache_dir=cache_dir,
max_artifact_size_mb=1,
warnings=warnings,
max_artifacts=100,
max_total_bytes=byte_limit,
max_elapsed_sec=10,
)
self.assertEqual(len(targets), 2)
self.assertEqual(prepare.call_count, 2)
self.assertEqual(usage.call_count, 1)
self.assertEqual(scanner.postman_cache_usage(cache_dir)['items'], 2)
self.assertTrue(any('aggregate byte limit' in warning for warning in warnings))
def test_elapsed_deadline_stops_walking_without_cache_publication(self):
with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, 'extract')
cache_dir = os.path.join(temp_dir, 'cache')
os.mkdir(extract_dir)
scanner.ensure_private_directory(cache_dir, reject_reparse=True)
for index in range(20):
Path(extract_dir, f'{index}.postman_collection.json').write_text(
json.dumps({'value': index}),
encoding='utf-8',
)
clock_value = 0.0
def advancing_clock():
nonlocal clock_value
clock_value += 0.1
return clock_value
warnings = []
patches = self._cache_patches(temp_dir, cache_dir)
with patches[0], patches[1], patches[2], patches[3], patches[4], \
mock.patch.object(scanner.time, 'monotonic', side_effect=advancing_clock), \
mock.patch.object(scanner, 'postman_cache_usage', wraps=scanner.postman_cache_usage) as usage:
targets = scanner.find_postman_artifacts(
extract_dir,
'npm',
{'name': 'fixture', 'version': '1.0.0'},
cache_dir=cache_dir,
max_artifact_size_mb=1,
warnings=warnings,
max_artifacts=100,
max_total_bytes=1024 * 1024,
max_elapsed_sec=0.35,
)
self.assertEqual(targets, [])
self.assertEqual(usage.call_count, 0)
self.assertEqual(scanner.postman_cache_usage(cache_dir)['items'], 0)
self.assertTrue(any('elapsed deadline' in warning for warning in warnings))
def test_batch_cache_capacity_failure_is_fail_closed_before_publication(self):
with tempfile.TemporaryDirectory() as temp_dir:
extract_dir = os.path.join(temp_dir, 'extract')
cache_dir = os.path.join(temp_dir, 'cache')
os.mkdir(extract_dir)
scanner.ensure_private_directory(cache_dir, reject_reparse=True)
for index in range(2):
Path(extract_dir, f'{index}.postman_collection.json').write_text(
json.dumps({'value': index}),
encoding='utf-8',
)
patches = self._cache_patches(temp_dir, cache_dir)
with patches[0], patches[1], patches[2], patches[3], patches[4], \
mock.patch.object(scanner.scan_config, 'postman_cache_max_items', 1), \
mock.patch.object(scanner, 'postman_cache_usage', wraps=scanner.postman_cache_usage) as usage, \
self.assertRaises(scanner.PostmanCacheCapacityError):
scanner.find_postman_artifacts(
extract_dir,
'npm',
{'name': 'fixture', 'version': '1.0.0'},
cache_dir=cache_dir,
max_artifact_size_mb=1,
max_artifacts=10,
max_total_bytes=1024 * 1024,
max_elapsed_sec=10,
)
self.assertEqual(usage.call_count, 1)
self.assertEqual(scanner.postman_cache_usage(cache_dir)['items'], 0)
def test_npm_and_pypi_harvest_warnings_preserve_trufflehog_findings(self):
finding = {'DetectorName': 'OpenAI', 'Raw': 'fixture-secret', 'Verified': False}
def limited_harvest(*args, **kwargs):
kwargs['warnings'].append('Optional Postman package harvesting degraded: fixture limit')
return []
cases = (
(
'npm',
scanner.scan_npm_package,
'parse_npm_target',
{'name': 'fixture', 'version': '1.0.0', 'tarball': 'https://example.invalid/package.tgz'},
'safe_extract_tar',
),
(
'pypi',
scanner.scan_pypi_package,
'parse_pypi_target',
{'name': 'fixture', 'version': '1.0.0', 'artifact': 'https://example.invalid/package.whl', 'size': 1},
'safe_extract_archive',
),
)
for source, scan, parser_name, package, extract_name in cases:
with self.subTest(source=source), tempfile.TemporaryDirectory() as work_dir, \
mock.patch.object(scanner, parser_name, return_value=package), \
mock.patch.object(scanner, 'create_command_work_dir', return_value=work_dir), \
mock.patch.object(scanner, 'cleanup_command_work_dir'), \
mock.patch.object(scanner, 'download_file', return_value=1), \
mock.patch.object(scanner, extract_name), \
mock.patch.object(scanner, 'harden_private_tree'), \
mock.patch.object(scanner, 'find_postman_artifacts', side_effect=limited_harvest), \
mock.patch.object(scanner, 'run_command_streamed', return_value=scanner.streamed_output_from_text(json.dumps(finding) + '\n', '', 0)), \
mock.patch.object(scanner, 'attach_nearby_context'):
result = scan('fixture-target', max_artifact_size_mb=1)
self.assertEqual(result['findings'], [finding])
self.assertTrue(any('fixture limit' in warning for warning in result['warnings']))
self.assertIn('postman_package_harvest', result['warning_classes'])
self.assertEqual(result['errors'], [])
def test_global_config_applies_canonical_harvest_limits(self):
configured = console_runner.load_config(str(APP_DIR / 'config.yaml'))['global']
self.assertEqual(configured['postman_package_harvest_max_artifacts'], 100)
self.assertEqual(configured['postman_package_harvest_max_bytes'], 128 * 1024 * 1024)
self.assertEqual(configured['postman_package_harvest_max_elapsed_sec'], 30)
previous = scanner.scan_config.__dict__.copy()
try:
console_runner.apply_global_config({
'postman_package_harvest_max_artifacts': 11,
'postman_package_harvest_max_bytes': 12345,
'postman_package_harvest_max_elapsed_sec': 2.5,
})
self.assertEqual(scanner.scan_config.postman_package_harvest_max_artifacts, 11)
self.assertEqual(scanner.scan_config.postman_package_harvest_max_bytes, 12345)
self.assertEqual(scanner.scan_config.postman_package_harvest_max_elapsed_sec, 2.5)
finally:
scanner.scan_config.__dict__.clear()
scanner.scan_config.__dict__.update(previous)
if __name__ == '__main__':
unittest.main()