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

483 lines
24 KiB
Python

import builtins
import hashlib
import json
import os
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
ROOT = Path(__file__).resolve().parents[1]
APP_DIR = ROOT / 'app'
sys.path.insert(0, str(APP_DIR))
import scanner
import scanner_db
from keycheckers import keycheck_common
class FindingLineSafetyTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
@staticmethod
def reader_env(state_dir):
return mock.patch.dict(os.environ, {
'KEYCHECK_OUTPUT_DIR': state_dir,
'KEYCHECK_STATE_DIR': state_dir,
'KEYCHECK_SERVICE': 'fixture',
'KEYCHECK_INPUT_TAIL_BYTES': '0',
'KEYCHECK_INPUT_MAX_LINE_BYTES': '1024',
})
def test_oversized_finding_projects_secret_free_marker_and_later_row_is_readable(self):
sentinel = 'DO-NOT-PROJECT-THIS-SECRET'
oversized_raw = sentinel * 100
oversized_uid = 'finding-oversized-0001'
later_uid = 'finding-later-0002'
result = {
'scan_event_id': 'scan-oversized-0001',
'target': 'fixture-target',
'scan_type': 'filesystem',
'timestamp': '2026-07-19T00:00:00+00:00',
'findings': [{
'finding_uid': oversized_uid,
'DetectorName': 'OpenAI',
'Raw': oversized_raw,
'RawV2': sentinel,
'StructuredData': {'token': sentinel},
'ScannerContext': {'nearby': sentinel, 'file': 'artifact.txt'},
'PostmanContext': {'endpoint': sentinel},
'SourceMetadata': {'Data': {'Filesystem': {
'file': 'artifact.txt', 'line': 7, 'commit': 'abc123',
}}},
}, {
'finding_uid': later_uid,
'DetectorName': 'Anthropic',
'Raw': 'small-later-secret',
}],
'errors': [],
}
with tempfile.TemporaryDirectory() as temp_dir:
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
results_dir = os.path.join(temp_dir, 'results')
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(results_dir, reject_reparse=True)
scanner.ensure_private_directory(state_dir, reject_reparse=True)
with mock.patch.object(scanner.scan_config, 'results_dir', results_dir), \
mock.patch.object(scanner.scan_config, 'jsonl_rotation_enabled', False), \
mock.patch.object(scanner.scan_config, 'keycheck_input_max_line_bytes', 1024), \
mock.patch.object(scanner, 'write_foundry_keycheck_candidates_from_findings', return_value=0):
self.assertTrue(scanner.save_scan_result(result))
findings_path = os.path.join(results_dir, 'found_secrets.jsonl')
raw_lines = Path(findings_path).read_bytes().splitlines(keepends=True)
self.assertEqual(len(raw_lines), 2)
self.assertTrue(all(len(line) <= 1024 for line in raw_lines))
marker = json.loads(raw_lines[0])
self.assertEqual(marker['finding_uid'], oversized_uid)
self.assertEqual(marker['DetectorName'], 'OpenAI')
self.assertTrue(marker['finding_omitted'])
self.assertTrue(marker['keycheck_uncheckable'])
self.assertEqual(marker['secret_sha256'], hashlib.sha256(sentinel.encode()).hexdigest())
self.assertEqual(marker['SourceIdentity']['file'], 'artifact.txt')
self.assertNotIn(sentinel, raw_lines[0].decode('utf-8'))
for forbidden in ('Raw', 'RawV2', 'StructuredData', 'ScannerContext', 'PostmanContext'):
self.assertNotIn(forbidden, marker)
scan_row = json.loads(Path(os.path.join(results_dir, 'scan_results.jsonl')).read_text(encoding='utf-8'))
self.assertTrue(any('oversized' in warning.lower() for warning in scan_row['warnings']))
self.assertNotIn(sentinel, json.dumps(scan_row))
self.assertIn(sentinel, result['findings'][0]['Raw'])
with self.reader_env(state_dir):
rows = [item['data'] for item in keycheck_common.iter_jsonl_input(findings_path)]
self.assertEqual([row['finding_uid'] for row in rows], [oversized_uid, later_uid])
def test_plain_legacy_oversized_row_is_skipped_and_checkpointed(self):
with tempfile.TemporaryDirectory() as temp_dir:
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
path = os.path.join(temp_dir, 'found_secrets.jsonl')
oversized = json.dumps({'finding_uid': 'legacy', 'Raw': 'x' * 3000}).encode() + b'\n'
later = b'{"finding_uid":"later"}\n'
Path(path).write_bytes(oversized + later)
scanner.harden_private_file(path)
with self.reader_env(state_dir):
rows = [item['data'] for item in keycheck_common.iter_jsonl_input(path)]
self.assertEqual(rows, [{'finding_uid': 'later'}])
state = json.loads(Path(os.path.join(state_dir, 'input_state.json')).read_text(encoding='utf-8'))
self.assertEqual(state['offset'], os.path.getsize(path))
self.assertEqual(state['skipped_oversized'], 1)
def test_segmented_legacy_oversized_row_does_not_block_segment_retirement(self):
with tempfile.TemporaryDirectory() as temp_dir:
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
current = os.path.join(temp_dir, 'found_secrets.jsonl')
segment = os.path.join(temp_dir, 'found_secrets.000001.jsonl')
oversized = json.dumps({'finding_uid': 'legacy', 'Raw': 'x' * 3000}).encode() + b'\n'
Path(segment).write_bytes(oversized + b'{"finding_uid":"segment-later"}\n')
Path(current).write_bytes(b'{"finding_uid":"current-later"}\n')
scanner.harden_private_file(segment)
scanner.harden_private_file(current)
with self.reader_env(state_dir):
rows = [item['data'] for item in keycheck_common.iter_jsonl_input(current)]
self.assertEqual(
[row['finding_uid'] for row in rows],
['segment-later', 'current-later'],
)
state = json.loads(Path(os.path.join(state_dir, 'input_state.json')).read_text(encoding='utf-8'))
segment_state = state['files'][os.path.abspath(segment)]
self.assertTrue(segment_state['done'])
self.assertEqual(segment_state['offset'], os.path.getsize(segment))
self.assertEqual(segment_state['skipped_oversized'], 1)
def test_torn_oversized_row_remains_fail_closed(self):
with tempfile.TemporaryDirectory() as temp_dir:
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
path = os.path.join(temp_dir, 'found_secrets.jsonl')
Path(path).write_bytes(b'{"finding_uid":"torn","Raw":"' + (b'x' * 3000))
scanner.harden_private_file(path)
with self.reader_env(state_dir):
with self.assertRaisesRegex(RuntimeError, 'torn oversized'):
list(keycheck_common.iter_jsonl_input(path))
self.assertFalse(os.path.exists(os.path.join(state_dir, 'input_state.json')))
def test_only_exact_resolved_corrupt_record_is_skipped(self):
with tempfile.TemporaryDirectory() as temp_dir:
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
current = os.path.join(temp_dir, 'found_secrets.jsonl')
segment = os.path.join(temp_dir, 'found_secrets.000001.jsonl')
first = b'{"finding_uid":"first"}\n'
corrupt = b'1, "legacy":"reviewed"}\n'
last = b'{"finding_uid":"last"}\n'
Path(segment).write_bytes(first + corrupt + last)
Path(current).write_bytes(b'{"finding_uid":"current"}\n')
scanner.harden_private_file(segment)
scanner.harden_private_file(current)
offset = len(first)
digest = hashlib.sha256(corrupt).hexdigest()
with self.assertRaisesRegex(scanner.JsonlProjectionReconciliationRequired, 'explicit review'):
scanner.reconcile_projection_ledger_batch(current, 'finding_uid')
scanner.approve_projection_reconciliation_issue(
current, 'finding_uid', os.path.basename(segment), offset, digest,
)
self.assertTrue(scanner.reconcile_projection_ledger_batch(current, 'finding_uid')['complete'])
with self.reader_env(state_dir):
rows = [item['data']['finding_uid'] for item in keycheck_common.iter_jsonl_input(current)]
self.assertEqual(rows, ['first', 'last', 'current'])
state = json.loads(Path(state_dir, 'input_state.json').read_text(encoding='utf-8'))
self.assertEqual(state['skipped_reviewed_corrupt'], 1)
def test_unreviewed_corrupt_record_remains_fail_closed(self):
with tempfile.TemporaryDirectory() as temp_dir:
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
path = os.path.join(temp_dir, 'found_secrets.jsonl')
Path(path).write_bytes(b'{"finding_uid":"first"}\ninvalid-json\n')
scanner.harden_private_file(path)
with self.reader_env(state_dir):
with self.assertRaisesRegex(RuntimeError, 'invalid committed keycheck input'):
list(keycheck_common.iter_jsonl_input(path))
def test_reviewed_corrupt_record_mutation_remains_fail_closed(self):
with tempfile.TemporaryDirectory() as temp_dir:
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
state_dir = os.path.join(temp_dir, 'state')
scanner.ensure_private_directory(state_dir, reject_reparse=True)
path = os.path.join(temp_dir, 'found_secrets.jsonl')
first = b'{"finding_uid":"first"}\n'
corrupt = b'invalid-one\n'
replacement = b'invalid-two\n'
self.assertEqual(len(corrupt), len(replacement))
Path(path).write_bytes(first + corrupt)
scanner.harden_private_file(path)
offset = len(first)
digest = hashlib.sha256(corrupt).hexdigest()
with self.assertRaises(scanner.JsonlProjectionReconciliationRequired):
scanner.reconcile_projection_ledger_batch(path, 'finding_uid')
scanner.approve_projection_reconciliation_issue(
path, 'finding_uid', os.path.basename(path), offset, digest,
)
self.assertTrue(scanner.reconcile_projection_ledger_batch(path, 'finding_uid')['complete'])
identity = os.stat(path, follow_symlinks=False)
with open(path, 'r+b') as handle:
handle.seek(offset)
handle.write(replacement)
handle.flush()
os.fsync(handle.fileno())
os.utime(path, ns=(identity.st_atime_ns, identity.st_mtime_ns))
with self.reader_env(state_dir):
with self.assertRaisesRegex(RuntimeError, 'reviewed keycheck corruption record changed'):
list(keycheck_common.iter_jsonl_input(path))
class OptionalContextSafetyTests(unittest.TestCase):
@classmethod
def setUpClass(cls):
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
def test_postman_parse_exception_retains_successful_trufflehog_finding(self):
finding = {'DetectorName': 'OpenAI', 'Raw': 'fixture-postman-secret'}
stdout = json.dumps(finding) + '\n'
with tempfile.TemporaryDirectory() as temp_dir:
cache_path = os.path.join(temp_dir, 'cache.json')
work_dir = os.path.join(temp_dir, 'work')
Path(cache_path).write_text('{"token":"fixture-postman-secret"}', encoding='utf-8')
scanner.ensure_private_directory(work_dir, reject_reparse=True)
target_data = {
'cache_path': cache_path,
'sha256': 'a' * 64,
'size': os.path.getsize(cache_path),
'kind': 'collection',
}
with mock.patch.object(scanner, 'parse_postman_target', return_value=target_data), \
mock.patch.object(scanner, 'validate_postman_cache_artifact', return_value=(cache_path, os.path.getsize(cache_path))), \
mock.patch.object(scanner, 'create_command_work_dir', return_value=work_dir), \
mock.patch.object(scanner, 'cleanup_command_work_dir'), \
mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \
mock.patch.object(scanner, 'run_command_streamed', return_value=scanner.streamed_output_from_text(stdout, '', 0)), \
mock.patch.object(scanner, 'load_postman_context', side_effect=scanner.PostmanCacheValidationError('fixture')):
result = scanner.scan_postman_target('fixture-target')
self.assertEqual(result['findings'], [finding])
self.assertFalse(result.get('structured_keycheck_pending', False))
self.assertTrue(result['context_enrichment_degraded'])
self.assertTrue(any('Postman JSON' in warning for warning in result['warnings']))
def test_bruno_text_artifact_skips_postman_json_context(self):
with tempfile.TemporaryDirectory() as temp_dir:
cache_path = os.path.join(temp_dir, 'request.bru')
work_dir = os.path.join(temp_dir, 'work')
Path(cache_path).write_text('meta {\n name: fixture\n}\n', encoding='utf-8')
scanner.ensure_private_directory(work_dir, reject_reparse=True)
target_data = {
'cache_path': cache_path,
'path': 'collection/request.bru',
'sha256': 'a' * 64,
'size': os.path.getsize(cache_path),
'kind': 'bruno',
}
with mock.patch.object(scanner, 'parse_postman_target', return_value=target_data), \
mock.patch.object(scanner, 'validate_postman_cache_artifact', return_value=(cache_path, os.path.getsize(cache_path))), \
mock.patch.object(scanner, 'create_command_work_dir', return_value=work_dir), \
mock.patch.object(scanner, 'cleanup_command_work_dir'), \
mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \
mock.patch.object(scanner, 'run_command_streamed', return_value=scanner.streamed_output_from_text('', '', 0)), \
mock.patch.object(scanner, 'load_postman_context', side_effect=AssertionError('JSON parser must not run')):
result = scanner.scan_postman_target('fixture-target')
self.assertEqual(result['findings'], [])
self.assertEqual(result['errors'], [])
self.assertFalse(result.get('context_enrichment_degraded', False))
self.assertFalse(result.get('degraded', False))
def test_nearby_context_reads_shared_file_once_for_many_findings(self):
with tempfile.TemporaryDirectory() as temp_dir:
source_path = os.path.join(temp_dir, 'source.txt')
Path(source_path).write_text(''.join(f'line-{index}\n' for index in range(100)), encoding='utf-8')
findings = [{
'DetectorName': 'OpenAI',
'Raw': f'secret-{index}',
'SourceMetadata': {'Data': {'Filesystem': {'file': source_path, 'line': index + 1}}},
} for index in range(20)]
result = {'findings': findings, 'errors': []}
real_open = builtins.open
source_reads = []
def counting_open(path, mode='r', *args, **kwargs):
if os.path.normcase(os.path.abspath(os.fspath(path))) == os.path.normcase(os.path.abspath(source_path)) and mode == 'rb':
source_reads.append(path)
return real_open(path, mode, *args, **kwargs)
with mock.patch.object(scanner.scan_config, 'context_enrichment_max_source_bytes', 4096), \
mock.patch.object(scanner.scan_config, 'context_enrichment_max_findings', 100), \
mock.patch.object(scanner.scan_config, 'context_enrichment_max_elapsed_sec', 30), \
mock.patch('builtins.open', side_effect=counting_open):
scanner.attach_nearby_context(result)
self.assertEqual(len(source_reads), 1)
self.assertTrue(all(finding.get('ScannerContext') for finding in findings))
def test_postman_comparison_budget_stops_work_without_dropping_findings(self):
with tempfile.TemporaryDirectory() as temp_dir:
cache_path = os.path.join(temp_dir, 'cache.json')
Path(cache_path).write_text('{}', encoding='utf-8')
findings = [
{'DetectorName': 'OpenAI', 'Raw': f'unmatched-secret-{index}'}
for index in range(5)
]
original = json.loads(json.dumps(findings))
contexts = [{
'path': f'$.values[{index}]',
'key': 'token',
'value': f'different-value-{index}',
'endpoint': '',
'host': '',
'auth_type': '',
'location': 'value',
} for index in range(10)]
with mock.patch.object(scanner.scan_config, 'context_enrichment_max_source_bytes', 1024), \
mock.patch.object(scanner.scan_config, 'context_enrichment_max_findings', 100), \
mock.patch.object(scanner.scan_config, 'context_enrichment_max_postman_comparisons', 3), \
mock.patch.object(scanner.scan_config, 'context_enrichment_max_elapsed_sec', 30):
budget = scanner.context_enrichment_budget()
with mock.patch.object(scanner, 'load_postman_context', return_value=contexts):
result = scanner.attach_postman_context({'findings': findings, 'errors': []}, cache_path, budget)
self.assertEqual(budget['postman_comparisons'], 3)
self.assertEqual(result['findings'], original)
self.assertTrue(result['structured_keycheck_pending'])
budget_warnings = [warning for warning in result['warnings'] if 'comparison budget' in warning]
self.assertEqual(len(budget_warnings), 1)
class PostmanEndpointSanitizationTests(unittest.TestCase):
USER_SENTINEL = 'POSTMAN-URL-USER-SENTINEL'
PASSWORD_SENTINEL = 'POSTMAN-URL-PASSWORD-SENTINEL'
API_KEY_SENTINEL = 'POSTMAN-QUERY-API-KEY-SENTINEL'
TOKEN_SENTINEL = 'POSTMAN-QUERY-TOKEN-SENTINEL'
QUERY_PASSWORD_SENTINEL = 'POSTMAN-QUERY-PASSWORD-SENTINEL'
FRAGMENT_SENTINEL = 'POSTMAN-FRAGMENT-SENTINEL'
SAFE_ENDPOINT = 'https://normal.openai.azure.com:443/openai/deployments/demo'
@classmethod
def setUpClass(cls):
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
@classmethod
def credentialed_endpoint(cls):
return (
f'https://{cls.USER_SENTINEL}:{cls.PASSWORD_SENTINEL}'
'@normal.openai.azure.com:443/openai/deployments/demo'
f'?api_key={cls.API_KEY_SENTINEL}&token={cls.TOKEN_SENTINEL}'
f'&password={cls.QUERY_PASSWORD_SENTINEL}#{cls.FRAGMENT_SENTINEL}'
)
@classmethod
def sentinels(cls):
return (
cls.USER_SENTINEL,
cls.PASSWORD_SENTINEL,
cls.API_KEY_SENTINEL,
cls.TOKEN_SENTINEL,
cls.QUERY_PASSWORD_SENTINEL,
cls.FRAGMENT_SENTINEL,
)
def test_scanner_context_removes_url_credentials_but_keeps_candidate_host(self):
detected_secret = 'detected-postman-secret'
contexts = [{
'path': '$.item[0].request.auth',
'key': 'api_key',
'value': detected_secret,
'endpoint': self.credentialed_endpoint(),
'host': 'normal.openai.azure.com',
'auth_type': 'apikey',
'location': 'header',
}]
with tempfile.TemporaryDirectory() as temp_dir:
cache_path = os.path.join(temp_dir, 'collection.json')
Path(cache_path).write_text('{}', encoding='utf-8')
with mock.patch.object(scanner, 'load_postman_context', return_value=contexts):
result = scanner.attach_postman_context({
'findings': [{'DetectorName': 'OpenAI', 'Raw': detected_secret}],
'errors': [],
}, cache_path)
context = result['findings'][0]['PostmanContext']
self.assertEqual(context['endpoint'], self.SAFE_ENDPOINT)
self.assertEqual(context['host'], 'normal.openai.azure.com')
persisted_finding = json.dumps(result['findings'][0])
self.assertTrue(all(sentinel not in persisted_finding for sentinel in self.sentinels()))
endpoints, _ = scanner.context_values_for_pairing(contexts)
self.assertIn('normal.openai.azure.com', endpoints)
def test_endpoint_sanitizer_validates_ports_and_caps_paths(self):
self.assertEqual(
scanner_db.sanitize_endpoint(
'https://user:password@normal.openai.azure.com:70000/path?token=secret'
),
'',
)
self.assertEqual(
scanner_db.sanitize_endpoint('normal.openai.azure.com:not-a-port/path?token=secret'),
'',
)
bounded = scanner_db.sanitize_endpoint(
'https://normal.openai.azure.com/' + ('a' * 5000) + '?token=PATH-QUERY-SENTINEL'
)
self.assertLessEqual(len(bounded), scanner_db.ENDPOINT_METADATA_MAX_CHARS)
self.assertTrue(bounded.startswith('https://normal.openai.azure.com/'))
self.assertNotIn('PATH-QUERY-SENTINEL', bounded)
self.assertEqual(scanner_db.sanitize_endpoint('not endpoint metadata'), '')
def test_enrichment_and_database_re_sanitize_imported_postman_context(self):
raw_endpoint = self.credentialed_endpoint()
finding = {
'DetectorName': 'OpenAI',
'Raw': 'detected-postman-secret',
'PostmanContext': {
'provider': 'openai',
'credential_kind': 'api_key',
'credential_confidence': 'detector_match',
'endpoint': raw_endpoint,
'host': raw_endpoint,
'json_path': raw_endpoint,
'legacy_url': raw_endpoint,
},
}
enriched = scanner_db.enrich_finding(finding)
self.assertEqual(enriched['endpoint'], self.SAFE_ENDPOINT)
self.assertEqual(enriched['resource'], self.SAFE_ENDPOINT)
self.assertTrue(all(sentinel not in json.dumps(enriched) for sentinel in self.sentinels()))
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(os.environ, {
'SCANNER_DB_URL': '',
'DATABASE_URL': '',
'TRUF_MANAGED_POSTGRES_DSN': '',
}):
db = scanner_db.ScannerDB(db_path=os.path.join(temp_dir, 'scanner.db'), db_url='')
try:
run_id = db.start_run('test', ['test'])
cycle_id = db.start_source_cycle(
run_id, 'fixture', 'postman', 'search', 'q', 1, 1, None, {}, {},
)
db.record_target_result(run_id, cycle_id, 'fixture', 'q', 'fixture-target', {
'findings': [finding],
'errors': [],
'scan_type': 'postman',
})
row = dict(db.conn.execute(
'''SELECT endpoint, resource, enrichment_json, raw_finding_json
FROM findings'''
).fetchone())
raw_result = db.conn.execute(
'SELECT raw_result_json FROM target_scans'
).fetchone()['raw_result_json']
finally:
db.close()
self.assertEqual(row['endpoint'], self.SAFE_ENDPOINT)
self.assertEqual(row['resource'], self.SAFE_ENDPOINT)
persisted = json.dumps(row) + raw_result
self.assertTrue(all(sentinel not in persisted for sentinel in self.sentinels()))
self.assertEqual(json.loads(row['enrichment_json'])['endpoint'], self.SAFE_ENDPOINT)
if __name__ == '__main__':
unittest.main()