3007 lines
144 KiB
Python
3007 lines
144 KiB
Python
import copy
|
|
from datetime import datetime, timedelta, timezone
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
import re
|
|
import sqlite3
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import threading
|
|
import time
|
|
from types import SimpleNamespace
|
|
import unittest
|
|
import uuid
|
|
from unittest import mock
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
APP_DIR = ROOT / 'app'
|
|
sys.path.insert(0, str(APP_DIR))
|
|
|
|
import scanner
|
|
import console_runner
|
|
import keycheck_runner
|
|
from keycheckers import keycheck_common
|
|
from keycheckers.anthropic import anthropicKeycheck as anthropic
|
|
from keycheckers.deepseek import deepseekKeycheck as deepseek
|
|
from keycheckers.qwen import qwenKeycheck as qwen
|
|
from result_spool import ResultSpool
|
|
from scanner_db import ScannerDB, normalize_target
|
|
from target_identity import parse_docker_target, serialize_docker_tag_target
|
|
|
|
|
|
def clear_scan_fatal():
|
|
with scanner._scan_slot_fatal_lock:
|
|
scanner._scan_slot_fatal_detail = None
|
|
scanner._scan_slot_fatal_event.clear()
|
|
|
|
|
|
class DeepSeekRecheckFlagTests(unittest.TestCase):
|
|
def test_retry_valid_is_supported_end_to_end(self):
|
|
self.assertIn('retry_valid', keycheck_runner.SERVICE_CAPABILITIES['deepseek']['flags'])
|
|
with mock.patch.object(sys, 'argv', ['deepseekKeycheck.py', '--retry-valid']):
|
|
args = deepseek.parse_args()
|
|
self.assertTrue(args.retry_valid)
|
|
|
|
|
|
class QwenRoutingPersistenceTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
|
|
|
|
def setUp(self):
|
|
clear_scan_fatal()
|
|
self.routing_db_temp = tempfile.TemporaryDirectory()
|
|
self.routing_db_path = os.path.join(self.routing_db_temp.name, 'scanner.db')
|
|
self.database_environment = mock.patch.dict(os.environ, {
|
|
'SCANNER_DB_URL': '', 'DATABASE_URL': '', 'TRUF_MANAGED_POSTGRES_DSN': '',
|
|
'KEYCHECK_DB_URL': '', 'KEYCHECK_DB_PATH': self.routing_db_path,
|
|
})
|
|
self.database_environment.start()
|
|
ScannerDB(db_path=self.routing_db_path, db_url='').close()
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
|
|
def tearDown(self):
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
clear_scan_fatal()
|
|
self.database_environment.stop()
|
|
self.routing_db_temp.cleanup()
|
|
|
|
def persist_finding_event(self, finding, target, clear_cache=True):
|
|
result = {'findings': [finding], 'errors': [], 'scan_type': 'fixture'}
|
|
db = ScannerDB(db_path=self.routing_db_path, initialize=False, db_url='')
|
|
try:
|
|
run_id = db.start_run('test', ['test'])
|
|
cycle_id = db.start_source_cycle(
|
|
run_id, 'fixture', 'fixture', 'search', target, 1, 1, None, {}, {},
|
|
)
|
|
db.record_target_result(run_id, cycle_id, 'fixture', target, target, result)
|
|
finally:
|
|
db.close()
|
|
if clear_cache:
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
return finding
|
|
|
|
def persist_provider_event(self, key, provider, target, clear_cache=True):
|
|
detector = {
|
|
'qwen': 'QwenDashScope',
|
|
'deepseek': 'DeepSeekApiKey',
|
|
'kimi': 'KimiMoonshot',
|
|
}[provider]
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': detector},
|
|
'Raw': key,
|
|
}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
return self.persist_finding_event(finding, target, clear_cache=clear_cache)
|
|
|
|
def assert_provider_request_counts(self, qwen_findings, deepseek_findings, qwen_count, deepseek_count):
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': {'message': 'unauthorized'}}
|
|
qwen_items = [
|
|
{'raw': finding.get('Raw', ''), 'source': 'roundtrip', 'finding': finding}
|
|
for finding in qwen_findings
|
|
]
|
|
deepseek_items = [
|
|
{'raw': finding.get('Raw', ''), 'source': 'roundtrip', 'finding': finding}
|
|
for finding in deepseek_findings
|
|
]
|
|
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=qwen_items), \
|
|
mock.patch.object(qwen.requests, 'get', return_value=response) as qwen_request, \
|
|
mock.patch.object(qwen.requests, 'post') as qwen_post:
|
|
candidates = list(qwen.iter_candidate_keys('unused.jsonl', []))
|
|
for candidate, _, _ in candidates:
|
|
qwen.check_key(candidate, [qwen.DEFAULT_BASE_URLS[0]], False, None, 1)
|
|
self.assertEqual(qwen_request.call_count, qwen_count)
|
|
qwen_post.assert_not_called()
|
|
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=deepseek_items), \
|
|
mock.patch.object(deepseek.requests, 'get', return_value=response) as deepseek_request:
|
|
candidates = list(deepseek.extract_candidates('unused.jsonl', []))
|
|
for candidate, _, _ in candidates:
|
|
deepseek.check_key(candidate, None, 1)
|
|
self.assertEqual(deepseek_request.call_count, deepseek_count)
|
|
|
|
def test_separate_events_without_context_route_weak_detector_to_resolver(self):
|
|
key = 'sk-' + ('1' * 32)
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
deepseek_finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [qwen_finding]})
|
|
scanner.strip_nearby_context_for_persistence({'findings': [qwen_finding]})
|
|
scanner.strip_nearby_context_for_persistence({'findings': [deepseek_finding]})
|
|
scanner.strip_nearby_context_for_persistence({'findings': [deepseek_finding]})
|
|
self.assertEqual(qwen_finding['ScannerContext']['provider_hint'], 'qwen')
|
|
self.assertEqual(
|
|
deepseek_finding['ScannerContext']['provider_hint'],
|
|
'ambiguous_generic_sk',
|
|
)
|
|
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 1, 0)
|
|
|
|
def test_explicit_qwen_context_routes_only_to_qwen(self):
|
|
key = 'sk-' + ('2' * 32)
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'ScannerContext': {'nearby': 'DASHSCOPE_API_KEY is configured here'},
|
|
}
|
|
deepseek_finding = {
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': key,
|
|
'ScannerContext': {'nearby': 'DASHSCOPE_API_KEY is configured here'},
|
|
}
|
|
for finding in (qwen_finding, deepseek_finding):
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
self.assertEqual(finding['ScannerContext']['provider_hint'], 'qwen')
|
|
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 1, 0)
|
|
|
|
def test_explicit_deepseek_metadata_routes_only_to_deepseek(self):
|
|
key = 'sk-' + ('3' * 32)
|
|
metadata = {'Data': {'Filesystem': {'file': 'config/deepseek.env'}}}
|
|
qwen_finding = {
|
|
'DetectorName': 'Qwen',
|
|
'Raw': key,
|
|
'SourceMetadata': copy.deepcopy(metadata),
|
|
}
|
|
deepseek_finding = {
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': key,
|
|
'SourceMetadata': copy.deepcopy(metadata),
|
|
}
|
|
for finding in (qwen_finding, deepseek_finding):
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
self.assertEqual(finding['ScannerContext']['provider_hint'], 'deepseek')
|
|
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 1)
|
|
|
|
def test_explicit_qwen_assignment_ignores_deepseek_filename_metadata(self):
|
|
key = 'sk-' + ('h' * 32)
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'SourceMetadata': {'Data': {'Filesystem': {'file': 'config/deepseek.env'}}},
|
|
}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
once = copy.deepcopy(finding)
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
|
|
self.assertEqual(finding, once)
|
|
self.assertEqual(finding['ScannerContext'], {
|
|
'provider_hint': 'qwen',
|
|
'provider_hint_source': 'explicit_assignment',
|
|
})
|
|
self.assert_provider_request_counts([finding], [finding], 1, 0)
|
|
|
|
def test_explicit_deepseek_assignment_ignores_qwen_filename_metadata(self):
|
|
key = 'sk-' + ('j' * 32)
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
'SourceMetadata': {'Data': {'Filesystem': {'file': 'config/qwen.env'}}},
|
|
}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
once = copy.deepcopy(finding)
|
|
scanner.strip_nearby_context_for_persistence({'findings': [finding]})
|
|
|
|
self.assertEqual(finding, once)
|
|
self.assertEqual(finding['ScannerContext'], {
|
|
'provider_hint': 'deepseek',
|
|
'provider_hint_source': 'explicit_assignment',
|
|
})
|
|
self.assert_provider_request_counts([finding], [finding], 0, 1)
|
|
|
|
def test_explicit_qwen_detector_beats_duplicate_broad_deepseek_sibling(self):
|
|
key = 'sk-' + ('g' * 32)
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
deepseek_finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
result = {'findings': [qwen_finding, deepseek_finding]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
once = copy.deepcopy(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(result, once)
|
|
self.assertEqual(
|
|
[finding['ScannerContext']['provider_hint'] for finding in result['findings']],
|
|
['qwen', 'qwen'],
|
|
)
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 1, 0)
|
|
|
|
def test_explicit_deepseek_detector_beats_duplicate_generic_qwen_sibling(self):
|
|
key = 'sk-' + ('z' * 32)
|
|
qwen_finding = {'DetectorName': 'Qwen', 'Raw': key}
|
|
deepseek_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
}
|
|
result = {'findings': [qwen_finding, deepseek_finding]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(
|
|
[finding['ScannerContext']['provider_hint'] for finding in result['findings']],
|
|
['deepseek', 'deepseek'],
|
|
)
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 1)
|
|
|
|
def test_independent_custom_detector_evidence_is_ambiguous(self):
|
|
key = 'sk-' + ('m' * 32)
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
deepseek_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
}
|
|
result = {'findings': [qwen_finding, deepseek_finding]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(
|
|
[finding['ScannerContext']['provider_hint'] for finding in result['findings']],
|
|
['ambiguous_qwen_deepseek', 'ambiguous_qwen_deepseek'],
|
|
)
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 0)
|
|
|
|
def test_generic_overlapping_detectors_without_context_route_to_resolver(self):
|
|
key = 'sk-' + ('n' * 32)
|
|
qwen_finding = {'DetectorName': 'Qwen', 'Raw': key}
|
|
deepseek_finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
result = {'findings': [qwen_finding, deepseek_finding]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
for finding in result['findings']:
|
|
self.assertEqual(
|
|
finding['ScannerContext']['provider_hint'],
|
|
'ambiguous_generic_sk',
|
|
)
|
|
self.assertEqual(
|
|
finding['ScannerContext']['provider_candidates'],
|
|
['deepseek', 'zai', 'qwen', 'kimi'],
|
|
)
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 0)
|
|
|
|
def test_qwen_rejects_anthropic_prefix_even_with_qwen_hint(self):
|
|
key = 'sk-ant-api03-' + ('A' * 93) + 'AA'
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'ScannerContext': {'provider_hint': 'qwen'},
|
|
}
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'fixture',
|
|
'finding': finding,
|
|
}]):
|
|
self.assertEqual(list(qwen.iter_candidate_keys('unused.jsonl', [])), [])
|
|
|
|
def test_mixed_provider_context_skips_both_providers(self):
|
|
key = 'sk-' + ('4' * 32)
|
|
context = {'nearby': 'QWEN_API_KEY is used beside api.deepseek.com'}
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'ScannerContext': copy.deepcopy(context),
|
|
}
|
|
deepseek_finding = {
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': key,
|
|
'ScannerContext': copy.deepcopy(context),
|
|
}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [qwen_finding]})
|
|
scanner.strip_nearby_context_for_persistence({'findings': [deepseek_finding]})
|
|
self.assertEqual(qwen_finding['ScannerContext']['provider_hint'], 'qwen')
|
|
self.assertEqual(
|
|
deepseek_finding['ScannerContext']['provider_hint'],
|
|
'ambiguous_qwen_deepseek',
|
|
)
|
|
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 1, 0)
|
|
|
|
def test_distinct_explicit_assignments_route_independently_in_mixed_env(self):
|
|
qwen_key = 'sk-' + ('u' * 32)
|
|
deepseek_key = 'sk-' + ('v' * 32)
|
|
nearby = 'QWEN_API_KEY=' + qwen_key + '\nDEEPSEEK_API_KEY=' + deepseek_key
|
|
qwen_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': qwen_key,
|
|
'ScannerContext': {'nearby': nearby, 'file': 'mixed.env'},
|
|
}
|
|
deepseek_finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': deepseek_key,
|
|
'ScannerContext': {'nearby': nearby, 'file': 'mixed.env'},
|
|
}
|
|
result = {'findings': [qwen_finding, deepseek_finding]}
|
|
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
once = copy.deepcopy(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
|
|
self.assertEqual(result, once)
|
|
self.assertEqual(qwen_finding['ScannerContext'], {
|
|
'file': 'mixed.env', 'provider_hint': 'qwen',
|
|
'provider_hint_source': 'explicit_assignment',
|
|
})
|
|
self.assertEqual(deepseek_finding['ScannerContext'], {
|
|
'file': 'mixed.env', 'provider_hint': 'deepseek',
|
|
'provider_hint_source': 'explicit_assignment',
|
|
})
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 1, 1)
|
|
|
|
def test_non_overlapping_qwen_format_does_not_require_provider_hint(self):
|
|
key = 'sk-sp-' + ('A' * 24)
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'fixture',
|
|
'finding': finding,
|
|
}]):
|
|
self.assertEqual(
|
|
[candidate for candidate, _, _ in qwen.iter_candidate_keys('unused.jsonl', [])],
|
|
[key],
|
|
)
|
|
|
|
def test_same_secret_is_ambiguous_across_repeated_strip_spool_db_and_jsonl(self):
|
|
key = 'sk-' + ('a' * 32)
|
|
event_id = str(uuid.uuid4())
|
|
result = {
|
|
'scan_event_id': event_id,
|
|
'target': 'fixture-target',
|
|
'scan_type': 'filesystem',
|
|
'timestamp': '2026-07-20T00:00:00+00:00',
|
|
'findings': [
|
|
{
|
|
'DetectorName': 'DeepSeekApiKey',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
'ScannerContext': {
|
|
'nearby': 'DEEPSEEK_API_KEY and QWEN_API_KEY are both configured here',
|
|
},
|
|
},
|
|
{
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'ScannerContext': {
|
|
'nearby': 'dashscope client beside api.deepseek.com',
|
|
},
|
|
},
|
|
],
|
|
'errors': [],
|
|
}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
once_stripped = copy.deepcopy(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(result, once_stripped)
|
|
for persisted in result['findings']:
|
|
self.assertEqual(
|
|
persisted['ScannerContext']['provider_hint'],
|
|
'ambiguous_qwen_deepseek',
|
|
)
|
|
self.assertNotIn('nearby', persisted['ScannerContext'])
|
|
self.assertNotIn(key, json.dumps(persisted['ScannerContext']))
|
|
|
|
roundtrips = [copy.deepcopy(result['findings'])]
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
spool = ResultSpool(os.path.join(temp_dir, 'spool'), min_free_bytes=0)
|
|
record = spool.write_event({
|
|
'version': 1,
|
|
'scan_event_id': event_id,
|
|
'result': result,
|
|
})
|
|
roundtrips.append(record.envelope['result']['findings'])
|
|
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'scanner.db'))
|
|
try:
|
|
run_id = db.start_run('test', ['test'])
|
|
cycle_id = db.start_source_cycle(
|
|
run_id, 'fixture', 'filesystem', 'search', 'q', 1, 1, None, {}, {},
|
|
)
|
|
db.record_target_result(run_id, cycle_id, 'fixture', 'q', 'fixture-target', result)
|
|
rows = db.conn.execute(
|
|
'SELECT raw_finding_json FROM findings ORDER BY id'
|
|
).fetchall()
|
|
roundtrips.append([json.loads(row['raw_finding_json']) for row in rows])
|
|
finally:
|
|
db.close()
|
|
|
|
results_dir = os.path.join(temp_dir, 'results')
|
|
scanner.ensure_private_directory(results_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, 'write_foundry_keycheck_candidates_from_findings', return_value=0):
|
|
self.assertTrue(scanner.save_scan_result(copy.deepcopy(result)))
|
|
jsonl_findings = [
|
|
json.loads(line) for line in
|
|
Path(os.path.join(results_dir, 'found_secrets.jsonl')).read_text(encoding='utf-8').splitlines()
|
|
if line.strip()
|
|
]
|
|
roundtrips.append(jsonl_findings)
|
|
|
|
for findings in roundtrips:
|
|
deepseek_finding = next(
|
|
finding for finding in findings if finding.get('DetectorName') == 'DeepSeekApiKey'
|
|
)
|
|
qwen_finding = next(
|
|
finding for finding in findings if finding.get('DetectorName') == 'CustomRegex'
|
|
)
|
|
self.assertEqual(
|
|
deepseek_finding['ScannerContext']['provider_hint'],
|
|
'ambiguous_qwen_deepseek',
|
|
)
|
|
self.assertEqual(
|
|
qwen_finding['ScannerContext']['provider_hint'],
|
|
'ambiguous_qwen_deepseek',
|
|
)
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'roundtrip',
|
|
'finding': deepseek_finding,
|
|
}]), mock.patch.object(deepseek.requests, 'get') as request:
|
|
candidates = list(deepseek.extract_candidates('unused.jsonl', []))
|
|
for candidate, _, _ in candidates:
|
|
deepseek.check_key(candidate, None, 1)
|
|
self.assertEqual(candidates, [])
|
|
request.assert_not_called()
|
|
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'roundtrip',
|
|
'finding': qwen_finding,
|
|
}]), mock.patch.object(qwen.requests, 'get') as get_request, \
|
|
mock.patch.object(qwen.requests, 'post') as post_request:
|
|
candidates = list(qwen.iter_candidate_keys('unused.jsonl', []))
|
|
for candidate, _, _ in candidates:
|
|
qwen.check_key(candidate, qwen.DEFAULT_BASE_URLS, False, None, 1)
|
|
self.assertEqual(candidates, [])
|
|
get_request.assert_not_called()
|
|
post_request.assert_not_called()
|
|
|
|
def test_deepseek_routing_decision_does_not_change_after_nearby_removal(self):
|
|
deepseek_key = 'sk-' + ('d' * 32)
|
|
result = {'findings': [
|
|
{
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': deepseek_key,
|
|
'ScannerContext': {'nearby': 'DEEPSEEK_API_KEY is configured here'},
|
|
},
|
|
{
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': 'sk-' + ('e' * 32),
|
|
},
|
|
]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(result['findings'][0]['ScannerContext'], {'provider_hint': 'deepseek'})
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(result['findings'][0]['ScannerContext'], {'provider_hint': 'deepseek'})
|
|
|
|
def test_legitimate_deepseek_candidate_still_makes_request(self):
|
|
key = 'sk-' + ('b' * 32)
|
|
finding = {
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': key,
|
|
'ScannerContext': {'nearby': 'DEEPSEEK_API_KEY is configured here'},
|
|
}
|
|
scanner.strip_nearby_context_for_persistence({'findings': [
|
|
finding,
|
|
]})
|
|
self.assertNotEqual(finding.get('ScannerContext', {}).get('provider_hint'), 'qwen')
|
|
self.assertFalse(deepseek.finding_looks_like_qwen_context(finding))
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': 'unauthorized'}
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'roundtrip',
|
|
'finding': finding,
|
|
}]), mock.patch.object(deepseek.requests, 'get', return_value=response) as request:
|
|
candidates = list(deepseek.extract_candidates('unused.jsonl', []))
|
|
self.assertEqual(len(candidates), 1)
|
|
deepseek.check_key(candidates[0][0], None, 1)
|
|
request.assert_called_once()
|
|
|
|
def test_postgres_deepseek_route_rejection_completes_as_no_context(self):
|
|
key = 'sk-' + ('f' * 32)
|
|
finding = {
|
|
'DetectorName': 'DeepSeek',
|
|
'Raw': key,
|
|
'ScannerContext': {'provider_hint': 'ambiguous_qwen_deepseek'},
|
|
}
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'postgres-fixture',
|
|
'finding': finding,
|
|
}]):
|
|
decisions = list(deepseek.iter_candidate_decisions('unused.jsonl', []))
|
|
self.assertEqual(len(decisions), 1)
|
|
self.assertEqual(decisions[0][3], 'ambiguous_qwen_deepseek')
|
|
result = deepseek.route_rejection_result(decisions[0][3])
|
|
self.assertEqual(result['status'], 'NO_CONTEXT')
|
|
self.assertEqual(keycheck_common.keycheck_status_group(result['status']), 'no_context')
|
|
self.assertEqual(
|
|
keycheck_runner.postgres_status_projection_targets('deepseek', 'NO_CONTEXT'),
|
|
['deepseekNoContext.txt'],
|
|
)
|
|
|
|
def test_legitimate_qwen_candidate_still_makes_request(self):
|
|
key = 'sk-' + ('c' * 32)
|
|
finding = {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
'ScannerContext': {'nearby': 'QWEN_API_KEY is configured here'},
|
|
}
|
|
result = {'findings': [finding]}
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
scanner.strip_nearby_context_for_persistence(result)
|
|
self.assertEqual(finding['ScannerContext']['provider_hint'], 'qwen')
|
|
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': {'message': 'unauthorized'}}
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'roundtrip',
|
|
'finding': finding,
|
|
}]), mock.patch.object(qwen.requests, 'get', return_value=response) as request, \
|
|
mock.patch.object(qwen.requests, 'post') as post_request:
|
|
candidates = list(qwen.iter_candidate_keys('unused.jsonl', []))
|
|
self.assertEqual(len(candidates), 1)
|
|
qwen.check_key(candidates[0][0], [qwen.DEFAULT_BASE_URLS[0]], False, None, 1)
|
|
request.assert_called_once()
|
|
post_request.assert_not_called()
|
|
|
|
def test_separate_database_events_with_conflicting_hints_route_neither(self):
|
|
key = 'sk-' + ('p' * 32)
|
|
qwen_finding = self.persist_provider_event(key, 'qwen', 'target-qwen')
|
|
deepseek_finding = self.persist_provider_event(key, 'deepseek', 'target-deepseek')
|
|
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 0)
|
|
|
|
def test_legacy_explicit_detector_identities_are_database_evidence(self):
|
|
fixtures = (
|
|
('qwen', 'a', {
|
|
'DetectorName': 'Qwen_DashScope',
|
|
'ScannerContext': {'provider_hint': 'qwen'},
|
|
}),
|
|
('qwen', 'b', {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'qwendashscope'},
|
|
}),
|
|
('deepseek', 'c', {
|
|
'DetectorName': 'DeepSeekApiKey',
|
|
'ScannerContext': {'provider_hint': 'deepseek'},
|
|
}),
|
|
('deepseek', 'd', {
|
|
'DetectorName': 'CustomRegex',
|
|
'ExtraData': {'name': 'DeepSeek_API_Key'},
|
|
}),
|
|
)
|
|
for expected, character, finding in fixtures:
|
|
with self.subTest(detector=finding):
|
|
key = 'sk-' + (character * 32)
|
|
finding['Raw'] = key
|
|
self.persist_finding_event(finding, f'legacy-{character}')
|
|
self.assertEqual(keycheck_common.global_provider_routing_evidence(key), {expected})
|
|
|
|
def test_empty_legacy_finding_json_uses_canonical_detector_identity(self):
|
|
key = 'sk-' + ('0' * 40)
|
|
rows = [
|
|
{'detector_name': 'DeepSeek', 'raw_finding_json': '', 'truncated': 0},
|
|
{'detector_name': 'QwenDashScope', 'raw_finding_json': '', 'truncated': 0},
|
|
]
|
|
with mock.patch.object(
|
|
ScannerDB, 'provider_routing_finding_rows', return_value=rows,
|
|
):
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
self.assertEqual(
|
|
keycheck_common.global_provider_routing_evidence(key),
|
|
{'qwen'},
|
|
)
|
|
|
|
def test_empty_legacy_finding_json_with_unknown_detector_fails_closed(self):
|
|
key = 'sk-' + ('9' * 40)
|
|
rows = [
|
|
{'detector_name': 'CustomRegex', 'raw_finding_json': '', 'truncated': 0},
|
|
]
|
|
with mock.patch.object(
|
|
ScannerDB, 'provider_routing_finding_rows', return_value=rows,
|
|
):
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
self.assertEqual(
|
|
keycheck_common.global_provider_routing_evidence(key),
|
|
{'qwen', 'deepseek', 'kimi', 'zai'},
|
|
)
|
|
|
|
def test_legacy_broad_deepseek_detector_and_its_extra_name_are_not_explicit_evidence(self):
|
|
key = 'sk-' + ('e' * 32)
|
|
self.persist_finding_event({
|
|
'DetectorName': 'DeepSeek',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
'ScannerContext': {'provider_hint': 'deepseek'},
|
|
}, 'legacy-broad-deepseek')
|
|
self.assertEqual(keycheck_common.global_provider_routing_evidence(key), set())
|
|
|
|
def test_legacy_qwen_and_current_deepseek_database_events_are_ambiguous(self):
|
|
key = 'sk-' + ('f' * 32)
|
|
self.persist_finding_event({
|
|
'DetectorName': 'QwenDashScope',
|
|
'Raw': key,
|
|
'ScannerContext': {'provider_hint': 'qwen'},
|
|
}, 'legacy-qwen')
|
|
self.persist_provider_event(
|
|
key, 'deepseek', 'current-deepseek', clear_cache=False,
|
|
)
|
|
self.assertEqual(
|
|
keycheck_common.global_provider_routing_evidence(key),
|
|
{'qwen', 'deepseek'},
|
|
)
|
|
|
|
def test_kimi_detector_is_persisted_generic_sk_routing_evidence(self):
|
|
key = 'sk-' + ('w' * 40)
|
|
self.persist_provider_event(key, 'kimi', 'current-kimi')
|
|
self.assertEqual(keycheck_common.global_provider_routing_evidence(key), {'kimi'})
|
|
|
|
def test_postgres_routing_merges_local_and_historical_provider_evidence(self):
|
|
key = 'sk-' + ('x' * 40)
|
|
self.persist_provider_event(key, 'kimi', 'current-kimi')
|
|
with mock.patch.dict(os.environ, {
|
|
'KEYCHECK_INPUT_MODE': 'postgres', 'KEYCHECK_SERVICE': 'qwen',
|
|
}, clear=False):
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
self.assertEqual(
|
|
keycheck_common.combined_provider_routing_hint(key, 'qwen'),
|
|
'ambiguous_generic_sk',
|
|
)
|
|
|
|
def test_empty_evidence_is_not_stale_after_concurrent_conflict_insert(self):
|
|
key = 'sk-' + ('i' * 32)
|
|
real_lookup = keycheck_common._load_provider_routing_evidence
|
|
with mock.patch.object(
|
|
keycheck_common, '_load_provider_routing_evidence', wraps=real_lookup,
|
|
) as lookup:
|
|
self.assertEqual(keycheck_common.combined_provider_routing_hint(key, 'qwen'), 'qwen')
|
|
self.persist_provider_event(
|
|
key, 'deepseek', 'concurrent-deepseek', clear_cache=False,
|
|
)
|
|
self.assertEqual(
|
|
keycheck_common.combined_provider_routing_hint(key, 'qwen'),
|
|
'ambiguous_qwen_deepseek',
|
|
)
|
|
self.assertEqual(lookup.call_count, 2)
|
|
|
|
def test_single_provider_evidence_is_not_stale_after_conflicting_insert(self):
|
|
key = 'sk-' + ('k' * 32)
|
|
self.persist_provider_event(key, 'qwen', 'first-qwen')
|
|
real_lookup = keycheck_common._load_provider_routing_evidence
|
|
with mock.patch.object(
|
|
keycheck_common, '_load_provider_routing_evidence', wraps=real_lookup,
|
|
) as lookup:
|
|
self.assertEqual(keycheck_common.global_provider_routing_evidence(key), {'qwen'})
|
|
self.persist_provider_event(
|
|
key, 'deepseek', 'concurrent-deepseek', clear_cache=False,
|
|
)
|
|
self.assertEqual(
|
|
keycheck_common.global_provider_routing_evidence(key),
|
|
{'qwen', 'deepseek'},
|
|
)
|
|
self.assertEqual(lookup.call_count, 2)
|
|
|
|
def test_qwen_deduplicates_routing_lookup_per_key_within_one_run(self):
|
|
key = 'sk-' + ('r' * 32)
|
|
finding = {
|
|
'DetectorName': 'QwenDashScope',
|
|
'Raw': key,
|
|
'ScannerContext': {
|
|
'provider_hint': 'qwen', 'provider_hint_source': 'explicit_assignment',
|
|
},
|
|
}
|
|
items = [
|
|
{'raw': key, 'source': f'fixture:{index}', 'finding': copy.deepcopy(finding)}
|
|
for index in range(100)
|
|
]
|
|
routing_common = sys.modules[qwen.combined_provider_routing_hint.__module__]
|
|
routing_common.clear_provider_routing_evidence_cache()
|
|
real_lookup = routing_common._load_provider_routing_evidence
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=items), mock.patch.object(
|
|
routing_common, '_load_provider_routing_evidence', wraps=real_lookup,
|
|
) as lookup:
|
|
candidates = list(qwen.iter_candidate_keys('unused.jsonl', []))
|
|
self.assertEqual([candidate for candidate, _, _ in candidates], [key] * len(items))
|
|
lookup.assert_called_once()
|
|
|
|
def test_qwen_recheck_uses_authenticated_candidate_route(self):
|
|
key = 'sk-' + ('v' * 32)
|
|
finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
item = {
|
|
'raw': key,
|
|
'source': 'postgres-fixture',
|
|
'finding': finding,
|
|
'candidate_metadata': {'provider_hint': 'qwen'},
|
|
}
|
|
args = SimpleNamespace(
|
|
input='unused.jsonl', plain=[], proxy_file='', timeout=1, max_keys=1,
|
|
base_url=[], no_default_base_urls=False, retry_network=False,
|
|
retry_limited=False, retry_unknown=False, retry_restricted=False,
|
|
retry_no_balance=False, retry_valid=True, recheck_all=False,
|
|
debug=False,
|
|
)
|
|
result = {'status': 'DEAD', 'message': 'fixture invalid key', 'models': []}
|
|
with mock.patch.dict(os.environ, {
|
|
'KEYCHECK_INPUT_MODE': 'postgres', 'KEYCHECK_SERVICE': 'qwen',
|
|
}, clear=False), mock.patch.object(
|
|
qwen, 'require_provider_authority',
|
|
), mock.patch.object(
|
|
qwen, 'parse_args', return_value=args,
|
|
), mock.patch.object(
|
|
qwen, 'ensure_files',
|
|
), mock.patch.object(
|
|
qwen, 'load_proxies', return_value=None,
|
|
), mock.patch.object(
|
|
qwen, 'load_checked_statuses', return_value={},
|
|
), mock.patch.object(
|
|
qwen, 'load_known_keys', return_value=set(),
|
|
), mock.patch.object(
|
|
qwen, 'iter_findings', return_value=iter([item]),
|
|
), mock.patch.object(
|
|
qwen, 'should_skip_key', return_value=False,
|
|
), mock.patch.object(
|
|
qwen, 'base_urls_from_args', return_value=(['https://fixture.invalid/v1'], False),
|
|
), mock.patch.object(
|
|
qwen, 'combined_provider_routing_hint', return_value='deepseek',
|
|
) as routing, mock.patch.object(
|
|
qwen, 'check_key', return_value=result,
|
|
) as check_key, mock.patch.object(
|
|
qwen, 'resolve_provider_key', return_value=result,
|
|
) as resolve, mock.patch.object(
|
|
qwen, 'write_result',
|
|
) as write_result:
|
|
qwen.main()
|
|
|
|
routing.assert_not_called()
|
|
check_key.assert_called_once()
|
|
resolve.assert_not_called()
|
|
self.assertNotIn(
|
|
qwen.CANDIDATE_PROVIDER_ROUTE_FIELD,
|
|
write_result.call_args.args[3],
|
|
)
|
|
|
|
def test_single_persisted_provider_hint_routes_only_that_provider(self):
|
|
for provider, character, expected in (
|
|
('qwen', 's', (1, 0)),
|
|
('deepseek', 'y', (0, 1)),
|
|
):
|
|
with self.subTest(provider=provider):
|
|
key = 'sk-' + (character * 32)
|
|
self.persist_provider_event(key, provider, f'target-{provider}')
|
|
qwen_finding = {'DetectorName': 'Qwen', 'Raw': key}
|
|
deepseek_finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
self.assert_provider_request_counts(
|
|
[qwen_finding], [deepseek_finding], *expected,
|
|
)
|
|
|
|
def test_database_query_failure_aborts_qwen_candidate_iteration(self):
|
|
key = 'sk-' + ('t' * 32)
|
|
qwen_finding = {
|
|
'DetectorName': 'QwenDashScope', 'Raw': key,
|
|
'ScannerContext': {
|
|
'provider_hint': 'qwen', 'provider_hint_source': 'explicit_assignment',
|
|
},
|
|
}
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
with mock.patch.object(
|
|
ScannerDB,
|
|
'provider_routing_finding_rows',
|
|
side_effect=RuntimeError('fixture query failure'),
|
|
) as lookup:
|
|
with mock.patch.object(qwen, 'iter_findings', return_value=[{
|
|
'raw': key, 'source': 'roundtrip', 'finding': qwen_finding,
|
|
}]), self.assertRaisesRegex(RuntimeError, 'routing evidence lookup failed'):
|
|
list(qwen.iter_candidate_keys('unused.jsonl', []))
|
|
lookup.assert_called_once()
|
|
|
|
def test_database_query_failure_aborts_deepseek_candidate_iteration(self):
|
|
key = 'sk-' + ('u' * 32)
|
|
deepseek_finding = {
|
|
'DetectorName': 'DeepSeekApiKey', 'Raw': key,
|
|
'ScannerContext': {
|
|
'provider_hint': 'deepseek', 'provider_hint_source': 'explicit_assignment',
|
|
},
|
|
}
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
with mock.patch.object(
|
|
ScannerDB,
|
|
'provider_routing_finding_rows',
|
|
side_effect=RuntimeError('fixture query failure'),
|
|
) as lookup, mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key, 'source': 'roundtrip', 'finding': deepseek_finding,
|
|
}]), mock.patch.object(deepseek.requests, 'get') as request:
|
|
with self.assertRaisesRegex(RuntimeError, 'routing evidence lookup failed closed'):
|
|
list(deepseek.extract_candidates('unused.jsonl', []))
|
|
lookup.assert_called_once()
|
|
request.assert_not_called()
|
|
|
|
def test_database_evidence_row_overflow_fails_closed(self):
|
|
key = 'sk-' + ('o' * 32)
|
|
self.persist_provider_event(key, 'qwen', 'target-one')
|
|
self.persist_provider_event(key, 'qwen', 'target-two')
|
|
qwen_finding = {'DetectorName': 'Qwen', 'Raw': key}
|
|
deepseek_finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
with mock.patch.dict(
|
|
os.environ, {'KEYCHECK_PROVIDER_EVIDENCE_MAX_ROWS': '1'}, clear=False,
|
|
):
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
self.assert_provider_request_counts([qwen_finding], [deepseek_finding], 0, 0)
|
|
|
|
|
|
class QwenCandidateBoundTests(unittest.TestCase):
|
|
@staticmethod
|
|
def args(input_file):
|
|
return SimpleNamespace(
|
|
input=input_file, plain=[], proxy_file='unused-proxy.txt', timeout=1,
|
|
max_keys=0, base_url=['https://fixture.invalid/v1'],
|
|
no_default_base_urls=True, retry_network=False, retry_limited=False,
|
|
retry_unknown=False, retry_restricted=False, retry_no_balance=False,
|
|
retry_valid=False, recheck_all=False, debug=False,
|
|
)
|
|
|
|
def test_concatenated_keys_are_not_qwen_candidates(self):
|
|
deepseek_key = 'sk-' + ('a1' * 16)
|
|
concatenated = deepseek_key + deepseek_key
|
|
self.assertEqual(qwen.key_from_text(concatenated), concatenated)
|
|
self.assertFalse(qwen.is_qwen_key(concatenated))
|
|
self.assertIn('multiple concatenated', qwen.qwen_key_rejection_reason(concatenated))
|
|
|
|
def test_openai_legacy_marker_makes_no_request_and_later_qwen_candidate_runs(self):
|
|
openai_key = 'sk-' + ('A' * 12) + qwen.OPENAI_LEGACY_KEY_MARKER + ('B' * 24)
|
|
qwen_key = 'sk-sp-' + ('Q' * 24)
|
|
findings = (
|
|
{'DetectorName': 'QwenDashScope', 'Raw': f'QWEN_API_KEY={openai_key}'},
|
|
{'DetectorName': 'QwenDashScope', 'Raw': f'QWEN_API_KEY={qwen_key}'},
|
|
)
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': {'message': 'unauthorized'}}
|
|
|
|
self.assertIn('OpenAI legacy', qwen.qwen_key_rejection_reason(openai_key))
|
|
self.assertEqual(qwen.key_from_text(f'QWEN_API_KEY={openai_key}'), '')
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(
|
|
''.join(json.dumps(finding) + '\n' for finding in findings),
|
|
encoding='utf-8',
|
|
)
|
|
scanner.harden_private_file(input_file)
|
|
checked_file = os.path.join(temp_dir, 'qwenChecked.txt')
|
|
results_file = os.path.join(temp_dir, 'qwenResults.jsonl')
|
|
status_files = {
|
|
status: os.path.join(temp_dir, f'qwen{status.title().replace("_", "")}.txt')
|
|
for status in qwen.STATUS_FILES
|
|
}
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_INLINE': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
'SCANNER_DB_URL': '',
|
|
'DATABASE_URL': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.multiple(
|
|
qwen,
|
|
CHECKED_FILE=checked_file,
|
|
RESULTS_FILE=results_file,
|
|
STATUS_FILES=status_files,
|
|
), mock.patch.object(qwen, 'require_provider_authority'), mock.patch.object(
|
|
qwen, 'parse_args', return_value=self.args(input_file),
|
|
), mock.patch.object(qwen, 'load_proxies', return_value=None), mock.patch.object(
|
|
qwen.requests, 'get', return_value=response,
|
|
) as provider_request, mock.patch.object(qwen.requests, 'post') as provider_post:
|
|
qwen.main()
|
|
|
|
provider_request.assert_called_once()
|
|
self.assertEqual(
|
|
provider_request.call_args.kwargs['headers']['Authorization'],
|
|
f'Bearer {qwen_key}',
|
|
)
|
|
provider_post.assert_not_called()
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
self.assertEqual(state['processed'], 2)
|
|
|
|
def test_8193_byte_explicit_key_is_skipped_later_key_runs_and_checkpoint_advances(self):
|
|
oversized = 'sk-' + ('a' * (8193 - 3))
|
|
valid = 'sk-sp-' + ('A' * 24)
|
|
findings = (
|
|
{'DetectorName': 'QwenDashScope', 'Raw': oversized},
|
|
{'DetectorName': 'QwenDashScope', 'Raw': valid},
|
|
)
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': {'message': 'unauthorized'}}
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(
|
|
''.join(json.dumps(finding) + '\n' for finding in findings),
|
|
encoding='utf-8',
|
|
)
|
|
scanner.harden_private_file(input_file)
|
|
checked_file = os.path.join(temp_dir, 'qwenChecked.txt')
|
|
results_file = os.path.join(temp_dir, 'qwenResults.jsonl')
|
|
status_files = {
|
|
status: os.path.join(temp_dir, f'qwen{status.title().replace("_", "")}.txt')
|
|
for status in qwen.STATUS_FILES
|
|
}
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_INLINE': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
'SCANNER_DB_URL': '',
|
|
'DATABASE_URL': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.multiple(
|
|
qwen,
|
|
CHECKED_FILE=checked_file,
|
|
RESULTS_FILE=results_file,
|
|
STATUS_FILES=status_files,
|
|
), mock.patch.object(qwen, 'require_provider_authority'), mock.patch.object(
|
|
qwen, 'parse_args', return_value=self.args(input_file),
|
|
), mock.patch.object(qwen, 'load_proxies', return_value=None), mock.patch.object(
|
|
qwen.requests, 'get', return_value=response,
|
|
) as provider_request, mock.patch.object(qwen.requests, 'post') as provider_post:
|
|
qwen.main()
|
|
|
|
self.assertIsNone(qwen.QWEN_KEY_REGEX.fullmatch(oversized))
|
|
provider_request.assert_called_once()
|
|
self.assertEqual(
|
|
provider_request.call_args.kwargs['headers']['Authorization'],
|
|
f'Bearer {valid}',
|
|
)
|
|
provider_post.assert_not_called()
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
self.assertEqual(state['processed'], 2)
|
|
self.assertFalse(os.path.exists(
|
|
keycheck_common.status_transaction_journal_path(checked_file),
|
|
))
|
|
|
|
def test_routing_lookup_failure_preserves_checkpoint_and_replays_candidate(self):
|
|
key = 'sk-' + ('m' * 32)
|
|
finding = {
|
|
'DetectorName': 'QwenDashScope',
|
|
'Raw': key,
|
|
'ScannerContext': {
|
|
'provider_hint': 'qwen',
|
|
'provider_hint_source': 'explicit_assignment',
|
|
},
|
|
}
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(json.dumps(finding) + '\n', encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.object(
|
|
qwen, 'require_provider_authority',
|
|
), mock.patch.object(
|
|
qwen, 'parse_args', side_effect=lambda: self.args(input_file),
|
|
), mock.patch.object(qwen, 'ensure_files'), mock.patch.object(
|
|
qwen, 'load_proxies', return_value=None,
|
|
), mock.patch.object(qwen, 'load_checked_statuses', return_value={}), mock.patch.object(
|
|
qwen, 'load_known_keys', return_value=set(),
|
|
), mock.patch.object(
|
|
qwen, 'check_key', return_value={'status': 'DEAD', 'message': 'fixture'},
|
|
) as check_key, mock.patch.object(qwen, 'write_result') as write_result:
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
with self.assertRaisesRegex(RuntimeError, 'routing evidence lookup failed'):
|
|
qwen.main()
|
|
self.assertFalse(os.path.exists(Path(temp_dir, 'input_state.json')))
|
|
check_key.assert_not_called()
|
|
write_result.assert_not_called()
|
|
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
keycheck_common.clear_provider_routing_evidence_cache()
|
|
with mock.patch.dict(os.environ, {'KEYCHECK_DB_PATH': db_path}, clear=False):
|
|
qwen.main()
|
|
|
|
check_key.assert_called_once_with(
|
|
key, ['https://fixture.invalid/v1'], True, None, 1, False,
|
|
)
|
|
write_result.assert_called_once()
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
|
|
def test_should_skip_failure_preserves_checkpoint_and_replays_candidate(self):
|
|
key = 'sk-sp-' + ('S' * 24)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(json.dumps({
|
|
'DetectorName': 'QwenDashScope', 'Raw': key,
|
|
}) + '\n', encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.object(
|
|
qwen, 'require_provider_authority',
|
|
), mock.patch.object(
|
|
qwen, 'parse_args', side_effect=lambda: self.args(input_file),
|
|
), mock.patch.object(qwen, 'ensure_files'), mock.patch.object(
|
|
qwen, 'load_proxies', return_value=None,
|
|
), mock.patch.object(qwen, 'load_checked_statuses', return_value={}), mock.patch.object(
|
|
qwen, 'load_known_keys', return_value=set(),
|
|
), mock.patch.object(
|
|
qwen, 'should_skip_key', side_effect=[RuntimeError('fixture preparation failure'), False],
|
|
), mock.patch.object(
|
|
qwen, 'check_key', return_value={'status': 'DEAD', 'message': 'fixture'},
|
|
) as check_key, mock.patch.object(qwen, 'write_result'):
|
|
with self.assertRaisesRegex(RuntimeError, 'fixture preparation failure'):
|
|
qwen.main()
|
|
self.assertFalse(os.path.exists(Path(temp_dir, 'input_state.json')))
|
|
qwen.main()
|
|
|
|
check_key.assert_called_once()
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
|
|
def test_candidate_publication_exception_preserves_checkpoint_and_retries_candidate(self):
|
|
keys = ('sk-sp-' + ('B' * 24), 'sk-sp-' + ('C' * 24))
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(''.join(
|
|
json.dumps({'DetectorName': 'QwenDashScope', 'Raw': key}) + '\n'
|
|
for key in keys
|
|
), encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.object(
|
|
qwen, 'require_provider_authority',
|
|
), mock.patch.object(
|
|
qwen, 'parse_args', side_effect=lambda: self.args(input_file),
|
|
), mock.patch.object(qwen, 'ensure_files'), mock.patch.object(
|
|
qwen, 'load_proxies', return_value=None,
|
|
), mock.patch.object(qwen, 'load_checked_statuses', return_value={}), mock.patch.object(
|
|
qwen, 'load_known_keys', return_value=set(),
|
|
), mock.patch.object(
|
|
qwen, 'check_key', return_value={'status': 'DEAD', 'message': 'fixture'},
|
|
) as check_key, mock.patch.object(
|
|
qwen, 'write_result', side_effect=[ValueError('fixture journal failure'), None, None],
|
|
) as write_result:
|
|
with self.assertRaisesRegex(ValueError, 'fixture journal failure'):
|
|
qwen.main()
|
|
self.assertFalse(os.path.exists(Path(temp_dir, 'input_state.json')))
|
|
qwen.main()
|
|
|
|
self.assertEqual([call.args[0] for call in check_key.call_args_list], [keys[0], *keys])
|
|
self.assertEqual([call.args[0] for call in write_result.call_args_list], [keys[0], *keys])
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
|
|
def test_result_segment_bound_failure_preserves_checkpoint_and_replays_candidate(self):
|
|
keys = ('sk-sp-' + ('D' * 24), 'sk-sp-' + ('E' * 24))
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(''.join(
|
|
json.dumps({'DetectorName': 'QwenDashScope', 'Raw': key}) + '\n'
|
|
for key in keys
|
|
), encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
checked_file = os.path.join(temp_dir, 'qwenChecked.txt')
|
|
results_file = os.path.join(temp_dir, 'qwenResults.jsonl')
|
|
segment_file = os.path.join(temp_dir, 'qwenResults.000001.jsonl')
|
|
Path(results_file).write_text('{}\n' * 350000, encoding='utf-8')
|
|
Path(segment_file).write_text('{"segment": true}\n', encoding='utf-8')
|
|
scanner.harden_private_file(results_file)
|
|
scanner.harden_private_file(segment_file)
|
|
status_files = {
|
|
status: os.path.join(temp_dir, f'qwen{status.title().replace("_", "")}.txt')
|
|
for status in qwen.STATUS_FILES
|
|
}
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_RESULTS_MAX_MB': '1',
|
|
'KEYCHECK_RESULTS_MAX_SEGMENTS': '1',
|
|
'KEYCHECK_DB_INLINE': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.multiple(
|
|
qwen,
|
|
CHECKED_FILE=checked_file,
|
|
RESULTS_FILE=results_file,
|
|
STATUS_FILES=status_files,
|
|
), mock.patch.object(qwen, 'require_provider_authority'), mock.patch.object(
|
|
qwen, 'parse_args', side_effect=lambda: self.args(input_file),
|
|
), mock.patch.object(qwen, 'load_proxies', return_value=None), mock.patch.object(
|
|
qwen, 'check_key', return_value={'status': 'DEAD', 'message': 'fixture'},
|
|
) as check_key:
|
|
with self.assertRaisesRegex(RuntimeError, 'segment bound reached'):
|
|
qwen.main()
|
|
self.assertFalse(os.path.exists(Path(temp_dir, 'input_state.json')))
|
|
|
|
os.remove(segment_file)
|
|
qwen.main()
|
|
|
|
self.assertEqual([call.args[0] for call in check_key.call_args_list], [keys[0], *keys])
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
|
|
def test_network_results_checkpoint_and_replay_in_retry_mode(self):
|
|
keys = ('sk-sp-' + ('N' * 24), 'sk-sp-' + ('R' * 24))
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
Path(input_file).write_text(''.join(
|
|
json.dumps({'DetectorName': 'QwenDashScope', 'Raw': key}) + '\n'
|
|
for key in keys
|
|
), encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
checked_file = os.path.join(temp_dir, 'qwenChecked.txt')
|
|
results_file = os.path.join(temp_dir, 'qwenResults.jsonl')
|
|
status_files = {
|
|
status: os.path.join(temp_dir, f'qwen{status.title().replace("_", "")}.txt')
|
|
for status in qwen.STATUS_FILES
|
|
}
|
|
first_args = self.args(input_file)
|
|
retry_args = self.args(input_file)
|
|
retry_args.retry_network = True
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_INLINE': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': '',
|
|
}
|
|
with mock.patch.dict(os.environ, environment, clear=False), mock.patch.multiple(
|
|
qwen,
|
|
CHECKED_FILE=checked_file,
|
|
RESULTS_FILE=results_file,
|
|
STATUS_FILES=status_files,
|
|
), mock.patch.object(qwen, 'require_provider_authority'), mock.patch.object(
|
|
qwen, 'parse_args', side_effect=[first_args, retry_args],
|
|
), mock.patch.object(qwen, 'load_proxies', return_value=None), mock.patch.object(
|
|
qwen.requests, 'get', side_effect=qwen.requests.ConnectionError('fixture transport failure'),
|
|
) as provider_request, mock.patch.object(qwen.requests, 'post') as provider_post:
|
|
qwen.main()
|
|
first_state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(first_state['offset'], os.path.getsize(input_file))
|
|
with mock.patch.dict(
|
|
os.environ, {'KEYCHECK_DISABLE_HIGH_WATERMARK': '1'}, clear=False,
|
|
):
|
|
qwen.main()
|
|
|
|
self.assertEqual(
|
|
[call.kwargs['headers']['Authorization'] for call in provider_request.call_args_list],
|
|
[f'Bearer {key}' for key in (*keys, *keys)],
|
|
)
|
|
provider_post.assert_not_called()
|
|
result_rows = [
|
|
json.loads(line) for line in Path(results_file).read_text(encoding='utf-8').splitlines()
|
|
]
|
|
self.assertEqual([row['status'] for row in result_rows], ['NETWORK'] * 4)
|
|
network_keys = [
|
|
keycheck_common.normalize_status_key(line)
|
|
for line in Path(status_files['NETWORK']).read_text(encoding='utf-8').splitlines()
|
|
]
|
|
self.assertEqual(network_keys, list(keys))
|
|
state = json.loads(Path(temp_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
self.assertEqual(state['offset'], os.path.getsize(input_file))
|
|
|
|
|
|
class QwenRegionalStatusTests(unittest.TestCase):
|
|
def test_status_output_is_utf8_safe_on_cp1251(self):
|
|
qwen_path = APP_DIR / 'keycheckers' / 'qwen' / 'qwenKeycheck.py'
|
|
script = "\n".join((
|
|
'import io, runpy, sys',
|
|
"sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='cp1251', errors='strict')",
|
|
f'runpy.run_path({str(qwen_path)!r}, run_name="qwen_encoding_fixture")',
|
|
"print(' STATUS: UNKNOWN | ' + ('x' * 200) + '\\uff08')",
|
|
))
|
|
completed = subprocess.run(
|
|
[sys.executable, '-B', '-c', script],
|
|
cwd=str(APP_DIR),
|
|
stdin=subprocess.DEVNULL,
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.PIPE,
|
|
check=False,
|
|
timeout=30,
|
|
)
|
|
self.assertEqual(completed.returncode, 0, completed.stderr.decode('utf-8', 'replace'))
|
|
self.assertIn('\uff08', completed.stdout.decode('utf-8', 'strict'))
|
|
|
|
def test_choose_final_status_has_deterministic_pairwise_priority(self):
|
|
priority = (
|
|
'VALID', 'NO_BALANCE', 'LIMITED', 'RESTRICTED',
|
|
'UNKNOWN', 'NO_CONTEXT', 'NETWORK', 'DEAD',
|
|
)
|
|
rank = {status: index for index, status in enumerate(priority)}
|
|
for left in priority:
|
|
for right in priority:
|
|
expected = left if rank[left] < rank[right] else right
|
|
with self.subTest(left=left, right=right):
|
|
self.assertEqual(
|
|
qwen.choose_final_status(
|
|
'sk-' + ('5' * 32),
|
|
[{'status': left}, {'status': right}],
|
|
False,
|
|
),
|
|
expected,
|
|
)
|
|
|
|
def test_network_and_dead_is_retryable_network_in_either_order(self):
|
|
for statuses in (('NETWORK', 'DEAD'), ('DEAD', 'NETWORK')):
|
|
with self.subTest(statuses=statuses):
|
|
self.assertEqual(
|
|
qwen.choose_final_status(
|
|
'sk-' + ('6' * 32),
|
|
[{'status': status} for status in statuses],
|
|
False,
|
|
),
|
|
'NETWORK',
|
|
)
|
|
|
|
def test_coding_plan_endpoint_is_enabled_by_default(self):
|
|
key = 'sk-sp-' + ('B' * 24)
|
|
self.assertEqual(qwen.DEFAULT_BASE_URLS[0], 'https://coding-intl.dashscope.aliyuncs.com/v1')
|
|
self.assertEqual(qwen.choose_final_status(key, [], False), 'DEAD')
|
|
self.assertEqual(qwen.choose_final_status(key, [{'status': 'DEAD'}], False), 'DEAD')
|
|
self.assertEqual(qwen.choose_final_status(key, [{'status': 'DEAD'}], True), 'DEAD')
|
|
|
|
def test_models_authentication_remains_valid_without_chat_model(self):
|
|
key = 'sk-' + ('M' * 40)
|
|
response = mock.Mock(status_code=200, text='{}')
|
|
response.json.return_value = {'data': [{'id': 'text-embedding-v3'}]}
|
|
with mock.patch.object(qwen.requests, 'get', return_value=response), \
|
|
mock.patch.object(qwen.requests, 'post') as post_request:
|
|
result = qwen.check_base_url(
|
|
key, 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
|
None, 1,
|
|
)
|
|
self.assertEqual(result['status'], 'VALID')
|
|
self.assertTrue(result['authenticated'])
|
|
self.assertEqual(result['llm_probe_status'], 'NO_CONTEXT')
|
|
post_request.assert_not_called()
|
|
|
|
def test_models_authentication_does_not_override_generation_failure(self):
|
|
key = 'sk-' + ('F' * 40)
|
|
models_response = mock.Mock(status_code=200, text='{}')
|
|
models_response.json.return_value = {'data': [{'id': 'qwen-plus'}]}
|
|
cases = (
|
|
(401, 'invalid_api_key', 'Invalid access token or token expired', 'DEAD'),
|
|
(400, 'Arrearage', 'The account is in arrears', 'NO_BALANCE'),
|
|
(429, 'Throttling.RateQuota', 'Request rate quota exceeded', 'LIMITED'),
|
|
(503, 'InternalError', 'Service unavailable', 'NETWORK'),
|
|
(400, 'InvalidParameter', 'Unsupported request', 'UNKNOWN'),
|
|
)
|
|
for http_status, code, message, expected in cases:
|
|
with self.subTest(expected=expected):
|
|
probe_response = mock.Mock(status_code=http_status, text=message)
|
|
probe_response.json.return_value = {
|
|
'error': {'code': code, 'message': message},
|
|
}
|
|
with mock.patch.object(qwen.requests, 'get', return_value=models_response), \
|
|
mock.patch.object(qwen.requests, 'post', return_value=probe_response):
|
|
result = qwen.check_base_url(
|
|
key, 'https://dashscope-intl.aliyuncs.com/compatible-mode/v1',
|
|
None, 1,
|
|
)
|
|
self.assertEqual(result['status'], expected)
|
|
self.assertEqual(result['llm_probe_status'], expected)
|
|
|
|
def test_valid_result_preserves_generation_probe_metadata(self):
|
|
key = 'sk-' + ('P' * 40)
|
|
attempt = {
|
|
'status': 'VALID', 'authenticated': True, 'region': 'fixture',
|
|
'base_url': 'https://fixture.invalid/v1', 'model_count': 2,
|
|
'models': ['qwen-plus'], 'llm_probe_status': 'GENERATION_OK',
|
|
'llm_probe_model': 'qwen-plus', 'message': 'ok',
|
|
}
|
|
with mock.patch.object(qwen, 'check_base_url', return_value=attempt):
|
|
result = qwen.check_key(
|
|
key, ['https://fixture.invalid/v1'], True, None, 1,
|
|
)
|
|
self.assertEqual(result['llm_probe_status'], 'GENERATION_OK')
|
|
self.assertEqual(result['llm_probe_model'], 'qwen-plus')
|
|
self.assertTrue(result['authenticated'])
|
|
|
|
def test_retry_network_status_prevents_should_skip(self):
|
|
key = 'sk-' + ('7' * 32)
|
|
args = SimpleNamespace(
|
|
recheck_all=False,
|
|
retry_network=True,
|
|
retry_limited=False,
|
|
retry_unknown=False,
|
|
retry_restricted=False,
|
|
retry_no_balance=False,
|
|
retry_valid=False,
|
|
)
|
|
retry_statuses = qwen.retry_statuses_from_args(args)
|
|
self.assertEqual(retry_statuses, {'NETWORK'})
|
|
self.assertFalse(qwen.should_skip_key(
|
|
key,
|
|
{key: 'NETWORK'},
|
|
{key},
|
|
args,
|
|
retry_statuses,
|
|
))
|
|
self.assertTrue(qwen.should_skip_key(
|
|
key,
|
|
{key: 'DEAD'},
|
|
{key},
|
|
args,
|
|
retry_statuses,
|
|
))
|
|
|
|
def test_forced_network_replay_attempts_each_key_once_and_max_counts_unique_keys(self):
|
|
first = 'sk-' + ('7' * 32)
|
|
second = 'sk-' + ('8' * 32)
|
|
third = 'sk-' + ('9' * 32)
|
|
args = SimpleNamespace(
|
|
input='unused.jsonl', plain=[], proxy_file='unused-proxy.txt', timeout=1,
|
|
max_keys=2, base_url=[], no_default_base_urls=False,
|
|
retry_network=True, retry_limited=False, retry_unknown=False,
|
|
retry_restricted=False, retry_no_balance=False, retry_valid=False,
|
|
recheck_all=False, debug=False,
|
|
)
|
|
candidates = [
|
|
(key, f'finding:{index}', {'DetectorName': 'QwenDashScope', 'Raw': key})
|
|
for key in (first, first, first, second, second, third)
|
|
for index in range(20)
|
|
]
|
|
checked = {first: 'NETWORK', second: 'NETWORK', third: 'NETWORK'}
|
|
|
|
with mock.patch.object(qwen, 'require_provider_authority'), \
|
|
mock.patch.object(qwen, 'parse_args', return_value=args), \
|
|
mock.patch.object(qwen, 'ensure_output_files'), \
|
|
mock.patch.object(qwen, 'load_proxies', return_value=None), \
|
|
mock.patch.object(qwen, 'load_checked_statuses', return_value=checked), \
|
|
mock.patch.object(qwen, 'load_known_keys', return_value=set(checked)), \
|
|
mock.patch.object(qwen, 'iter_candidate_keys', return_value=iter(candidates)), \
|
|
mock.patch.object(qwen, 'check_key', return_value={
|
|
'status': 'NETWORK', 'message': 'fixture network failure',
|
|
}) as check_key, \
|
|
mock.patch.object(qwen, 'write_result'), \
|
|
mock.patch.object(
|
|
sys.modules[qwen.should_skip_key.__module__],
|
|
'record_cached_keycheck_occurrence',
|
|
) as cached:
|
|
qwen.main()
|
|
|
|
self.assertEqual([call.args[0] for call in check_key.call_args_list], [first, second])
|
|
self.assertGreater(cached.call_count, 1)
|
|
|
|
|
|
class QwenDurableRetryInputTests(unittest.TestCase):
|
|
@staticmethod
|
|
def args(input_file):
|
|
return SimpleNamespace(
|
|
input=input_file, plain=[], proxy_file='unused-proxy.txt', timeout=1,
|
|
max_keys=0, base_url=[], no_default_base_urls=False,
|
|
retry_network=True, retry_limited=False, retry_unknown=False,
|
|
retry_restricted=False, retry_no_balance=False, retry_valid=False,
|
|
recheck_all=False, debug=False,
|
|
)
|
|
|
|
def test_provider_network_backlog_retries_without_retained_finding_and_success_clears_it(self):
|
|
key = 'sk-' + ('k' * 32)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'found_secrets.jsonl')
|
|
Path(input_file).write_text('', encoding='utf-8')
|
|
scanner.harden_private_file(input_file)
|
|
status_files = {
|
|
status: os.path.join(temp_dir, f'qwen{status.title().replace("_", "")}.txt')
|
|
for status in qwen.STATUS_FILES
|
|
}
|
|
for path in status_files.values():
|
|
Path(path).write_text('', encoding='utf-8')
|
|
scanner.harden_private_file(path)
|
|
Path(status_files['NETWORK']).write_text(f'{key}\tNETWORK\n', encoding='utf-8')
|
|
scanner.harden_private_file(status_files['NETWORK'])
|
|
checked_file = os.path.join(temp_dir, 'qwenChecked.txt')
|
|
results_file = os.path.join(temp_dir, 'qwenResults.jsonl')
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
environment = {
|
|
'KEYCHECK_OUTPUT_DIR': temp_dir,
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': db_path,
|
|
}
|
|
|
|
with mock.patch.dict(os.environ, environment, clear=False), \
|
|
mock.patch.multiple(
|
|
qwen, STATUS_FILES=status_files, CHECKED_FILE=checked_file,
|
|
RESULTS_FILE=results_file,
|
|
), mock.patch.object(qwen, 'require_provider_authority'), \
|
|
mock.patch.object(qwen, 'parse_args', side_effect=lambda: self.args(input_file)), \
|
|
mock.patch.object(qwen, 'load_proxies', return_value=None), \
|
|
mock.patch.object(qwen, 'check_key', return_value={
|
|
'status': 'VALID', 'message': 'fixture success', 'models': [],
|
|
}) as check_key, mock.patch.object(qwen, 'write_keycheck_event'), \
|
|
mock.patch.object(qwen, 'record_validation_result', return_value=True):
|
|
qwen.main()
|
|
qwen.main()
|
|
|
|
check_key.assert_called_once()
|
|
self.assertNotIn(key, Path(status_files['NETWORK']).read_text(encoding='utf-8'))
|
|
self.assertIn(key, Path(status_files['VALID']).read_text(encoding='utf-8'))
|
|
|
|
def test_only_provider_owned_retry_file_bypasses_overlap_context_gate(self):
|
|
key = 'sk-' + ('l' * 32)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
input_file = os.path.join(temp_dir, 'found_secrets.jsonl')
|
|
plain_file = os.path.join(temp_dir, 'attacker-plain.txt')
|
|
network_file = os.path.join(temp_dir, 'qwenNetwork.txt')
|
|
for path, text in (
|
|
(input_file, ''), (plain_file, key + '\n'), (network_file, key + '\tNETWORK\n'),
|
|
):
|
|
Path(path).write_text(text, encoding='utf-8')
|
|
status_files = dict(qwen.STATUS_FILES, NETWORK=network_file)
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
with mock.patch.dict(os.environ, {
|
|
'KEYCHECK_DB_URL': '', 'KEYCHECK_DB_PATH': db_path,
|
|
}, clear=False), mock.patch.object(
|
|
qwen, 'STATUS_FILES', status_files,
|
|
), mock.patch.object(qwen, 'iter_findings', return_value=[]):
|
|
candidates = list(qwen.iter_candidate_keys(
|
|
input_file, [plain_file], [network_file],
|
|
))
|
|
self.assertEqual([(candidate, source) for candidate, source, _ in candidates], [(key, network_file)])
|
|
|
|
|
|
class ProviderDetectorPolicyTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
import yaml
|
|
|
|
policy = yaml.safe_load((APP_DIR / 'trufflehog-custom-detectors.yaml').read_text(encoding='utf-8'))
|
|
cls.detectors = {item['name']: item for item in policy['detectors']}
|
|
|
|
def test_qwen_detector_requires_explicit_assignment(self):
|
|
detector = self.detectors['QwenDashScope']
|
|
self.assertEqual(set(detector['keywords']), {'QWEN_API_KEY', 'DASHSCOPE_API_KEY'})
|
|
self.assertEqual(len(detector['regex']), 1)
|
|
pattern = re.compile(next(iter(detector['regex'].values())))
|
|
key = 'sk-' + ('q' * 32)
|
|
for text in (f'QWEN_API_KEY={key}', f'"DASHSCOPE_API_KEY": "{key}"'):
|
|
with self.subTest(text=text):
|
|
self.assertEqual(pattern.search(text).group(1), key)
|
|
self.assertIsNone(pattern.search(f'ANTHROPIC_API_KEY=sk-ant-example; model=qwen; key={key}'))
|
|
self.assertIsNone(pattern.search(f'qwen_model = "qwen-plus"\nOPENAI_API_KEY={key}'))
|
|
longest = 'sk-sp-' + ('a' * (qwen.QWEN_KEY_MAX_BYTES - len('sk-sp-')))
|
|
oversized = longest + 'a'
|
|
self.assertEqual(len(longest.encode('ascii')), qwen.QWEN_KEY_MAX_BYTES)
|
|
self.assertEqual(pattern.search(f'QWEN_API_KEY={longest}').group(1), longest)
|
|
self.assertIsNone(pattern.search(f'QWEN_API_KEY={oversized}'))
|
|
self.assertIsNone(pattern.search('QWEN_API_KEY=' + ('sk-' + ('a' * 8190))))
|
|
hyphenated_oversized = 'sk-' + ('a' * 506) + '-' + ('a' * 100)
|
|
self.assertIsNone(qwen.QWEN_KEY_REGEX.search(hyphenated_oversized))
|
|
self.assertTrue(qwen.is_qwen_key(longest))
|
|
self.assertFalse(qwen.is_qwen_key(oversized))
|
|
|
|
def test_deepseek_detector_requires_canonical_assignment(self):
|
|
detector = self.detectors['DeepSeekApiKey']
|
|
self.assertEqual(detector['keywords'], ['DEEPSEEK_API_KEY'])
|
|
pattern = re.compile(next(iter(detector['regex'].values())))
|
|
key = 'sk-' + ('z9' * 16)
|
|
self.assertEqual(pattern.search(f'DEEPSEEK_API_KEY="{key}"').group(1), key)
|
|
self.assertEqual(pattern.search(f'"DEEPSEEK_API_KEY": "{key}"').group(1), key)
|
|
self.assertIsNone(pattern.search('DEEPSEEK_API_KEY=sk-' + ('A' * 32)))
|
|
self.assertIsNone(pattern.search('DEEPSEEK_MODEL=deepseek-chat'))
|
|
|
|
|
|
class ProviderFormatTests(unittest.TestCase):
|
|
def test_deepseek_accepts_lowercase_alphanumeric_with_explicit_evidence(self):
|
|
key = 'sk-' + ('z9' * 16)
|
|
finding = {
|
|
'DetectorName': 'DeepSeekApiKey',
|
|
'ExtraData': {'name': 'DeepSeekApiKey'},
|
|
'Raw': key,
|
|
}
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'fixture',
|
|
'finding': finding,
|
|
}]), mock.patch.object(
|
|
deepseek, 'combined_provider_routing_hint', side_effect=lambda _, hint: hint,
|
|
):
|
|
self.assertEqual(
|
|
[candidate for candidate, _, _ in deepseek.extract_candidates('unused.jsonl', [])],
|
|
[key],
|
|
)
|
|
|
|
def test_deepseek_builtin_shape_without_evidence_is_rejected(self):
|
|
key = 'sk-' + ('z9' * 16)
|
|
finding = {'DetectorName': 'DeepSeek', 'Raw': key}
|
|
with mock.patch.object(deepseek, 'iter_findings', return_value=[{
|
|
'raw': key,
|
|
'source': 'fixture',
|
|
'finding': finding,
|
|
}]), mock.patch.object(
|
|
deepseek, 'combined_provider_routing_hint', return_value='',
|
|
), mock.patch.object(deepseek, 'provider_routing_database_failed', return_value=False):
|
|
self.assertEqual(list(deepseek.extract_candidates('unused.jsonl', [])), [])
|
|
|
|
def test_anthropic_extracts_api03_admin01_and_legacy_formats(self):
|
|
keys = (
|
|
'sk-ant-api03-' + ('A' * 93) + 'AA',
|
|
'sk-ant-admin01-' + ('b' * 93) + 'AA',
|
|
'sk-ant-' + ('C' * 86),
|
|
)
|
|
items = [
|
|
{'raw': key, 'source': 'fixture', 'finding': {'DetectorName': 'Anthropic', 'Raw': key}}
|
|
for key in keys
|
|
]
|
|
with mock.patch.object(anthropic, 'iter_findings', return_value=items):
|
|
self.assertEqual(
|
|
[candidate for candidate, _, _ in anthropic.extract_candidates('unused.jsonl', [])],
|
|
list(keys),
|
|
)
|
|
self.assertIsNone(anthropic.ANTHROPIC_REGEX.fullmatch('sk-ant-admin01-' + ('b' * 92) + 'AA'))
|
|
|
|
|
|
class AnthropicAdminValidationTests(unittest.TestCase):
|
|
def setUp(self):
|
|
self.key = 'sk-ant-admin01-' + ('b' * 93) + 'AA'
|
|
self.proxy = {'https': 'http://proxy.invalid'}
|
|
|
|
@staticmethod
|
|
def response(status_code):
|
|
response = mock.Mock(status_code=status_code, text='fixture error')
|
|
response.json.return_value = {'error': {'message': 'fixture error'}}
|
|
return response
|
|
|
|
def test_admin_key_uses_only_read_only_admin_list_endpoint(self):
|
|
with mock.patch.object(
|
|
anthropic.requests, 'get', return_value=self.response(200),
|
|
) as get_request, mock.patch.object(anthropic.requests, 'post') as post_request:
|
|
result = anthropic.check_key(
|
|
self.key, self.proxy, 7, model='must-not-be-used', include_models=True,
|
|
)
|
|
|
|
self.assertEqual(result['status'], 'VALID')
|
|
self.assertTrue(result['admin'])
|
|
get_request.assert_called_once_with(
|
|
'https://api.anthropic.com/v1/organizations/api_keys',
|
|
headers={
|
|
'anthropic-version': '2023-06-01',
|
|
'x-api-key': self.key,
|
|
},
|
|
params={'limit': 1},
|
|
proxies=self.proxy,
|
|
timeout=7,
|
|
)
|
|
post_request.assert_not_called()
|
|
|
|
def test_admin_http_statuses_are_classified_without_messages_call(self):
|
|
expected = {
|
|
401: 'DEAD',
|
|
403: 'RESTRICTED',
|
|
429: 'LIMITED',
|
|
500: 'UNKNOWN',
|
|
}
|
|
for status_code, status in expected.items():
|
|
with self.subTest(status_code=status_code), mock.patch.object(
|
|
anthropic.requests, 'get', return_value=self.response(status_code),
|
|
), mock.patch.object(anthropic.requests, 'post') as post_request:
|
|
result = anthropic.check_key(self.key, None, 3)
|
|
self.assertEqual(result['status'], status)
|
|
self.assertEqual(result['http_status'], status_code)
|
|
post_request.assert_not_called()
|
|
|
|
def test_admin_transport_failure_is_network(self):
|
|
with mock.patch.object(
|
|
anthropic.requests, 'get',
|
|
side_effect=anthropic.requests.ConnectionError('fixture transport failure'),
|
|
), mock.patch.object(anthropic.requests, 'post') as post_request:
|
|
result = anthropic.check_key(self.key, None, 3)
|
|
self.assertEqual(result['status'], 'NETWORK')
|
|
self.assertTrue(result['admin'])
|
|
post_request.assert_not_called()
|
|
|
|
def test_standard_api_key_keeps_messages_validation(self):
|
|
key = 'sk-ant-api03-' + ('A' * 93) + 'AA'
|
|
with mock.patch.object(anthropic.requests, 'get') as get_request, mock.patch.object(
|
|
anthropic.requests, 'post', return_value=self.response(401),
|
|
) as post_request:
|
|
result = anthropic.check_key(key, None, 5, model='claude-fixture')
|
|
self.assertEqual(result['status'], 'DEAD')
|
|
post_request.assert_called_once()
|
|
self.assertEqual(post_request.call_args.args[0], 'https://api.anthropic.com/v1/messages')
|
|
self.assertEqual(post_request.call_args.kwargs['headers'], {
|
|
'content-type': 'application/json',
|
|
'anthropic-version': '2023-06-01',
|
|
'x-api-key': key,
|
|
})
|
|
get_request.assert_not_called()
|
|
|
|
|
|
class KeycheckRetryReplayTests(unittest.TestCase):
|
|
@staticmethod
|
|
def args(**overrides):
|
|
values = {
|
|
'config': 'config.yaml', 'input': None, 'input_mode': 'jsonl',
|
|
'proxy_file': None, 'max_keys': 0,
|
|
'retry_network': False, 'retry_limited': False, 'retry_unknown': False,
|
|
'retry_restricted': False, 'retry_no_balance': False, 'retry_valid': False,
|
|
'no_resource_probe': False, 'recheck_all': False,
|
|
}
|
|
values.update(overrides)
|
|
return SimpleNamespace(**values)
|
|
|
|
@staticmethod
|
|
def write_private(path, text):
|
|
Path(path).write_text(text, encoding='utf-8')
|
|
scanner.harden_private_file(path)
|
|
|
|
def captured_runner_env(self, temp_dir, args, status_text=None):
|
|
results = os.path.join(temp_dir, 'results')
|
|
keychecks = os.path.join(temp_dir, 'keychecks')
|
|
output = os.path.join(keychecks, 'qwen')
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
for path in (temp_dir, results, keychecks, output):
|
|
scanner.ensure_private_directory(path, reject_reparse=True)
|
|
if not os.path.exists(db_path):
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
if status_text is not None:
|
|
self.write_private(os.path.join(output, 'qwenNetwork.txt'), status_text)
|
|
layout = {
|
|
'results_dir': results, 'keycheck_dir': keychecks,
|
|
'proxy_file': os.path.join(temp_dir, 'proxy.txt'), 'database_path': db_path,
|
|
}
|
|
|
|
class CapturedProcess:
|
|
def wait(self):
|
|
return 0
|
|
|
|
with mock.patch.object(keycheck_runner, 'require_active_supervisor_child', return_value={}), \
|
|
mock.patch.object(keycheck_runner, 'supervised_child_environment', return_value={}), \
|
|
mock.patch.object(keycheck_runner, 'OwnedProcess', return_value=CapturedProcess()) as owned, \
|
|
mock.patch.dict(os.environ, {
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '1',
|
|
'TRUF_MANAGED_POSTGRES_DSN': '', 'SCANNER_DB_URL': '', 'DATABASE_URL': '',
|
|
'KEYCHECK_DB_URL': '',
|
|
}, clear=False):
|
|
self.assertEqual(keycheck_runner.run_service(
|
|
'qwen', keycheck_runner.SERVICES['qwen'], args, layout,
|
|
), 0)
|
|
return owned.call_args.kwargs['env']
|
|
|
|
def test_network_backlog_enables_replay_for_provider_invocation(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
env = self.captured_runner_env(temp_dir, self.args(retry_network=True), 'candidate\tNETWORK\n')
|
|
self.assertEqual(env.get('KEYCHECK_DISABLE_HIGH_WATERMARK'), '1')
|
|
|
|
def test_no_network_backlog_preserves_incremental_high_watermark(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
env = self.captured_runner_env(temp_dir, self.args(retry_network=True), '')
|
|
self.assertNotIn('KEYCHECK_DISABLE_HIGH_WATERMARK', env)
|
|
|
|
def test_recheck_all_always_enables_replay(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
env = self.captured_runner_env(temp_dir, self.args(recheck_all=True))
|
|
self.assertEqual(env.get('KEYCHECK_DISABLE_HIGH_WATERMARK'), '1')
|
|
|
|
def test_network_at_eof_is_visible_on_retry_replay(self):
|
|
key = 'sk-' + ('r' * 32)
|
|
finding = {
|
|
'DetectorName': 'QwenDashScope',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
input_file = os.path.join(temp_dir, 'findings.jsonl')
|
|
self.write_private(input_file, json.dumps(finding) + '\n')
|
|
status_file = os.path.join(temp_dir, 'qwenNetwork.txt')
|
|
self.write_private(status_file, '')
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
environment = {
|
|
'KEYCHECK_STATE_DIR': temp_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': db_path,
|
|
}
|
|
attempts = []
|
|
|
|
def consume():
|
|
for candidate, _, _ in qwen.iter_candidate_keys(input_file, []):
|
|
attempts.append(candidate)
|
|
|
|
with mock.patch.dict(os.environ, environment, clear=False):
|
|
consume()
|
|
consume()
|
|
self.assertEqual(attempts, [key])
|
|
self.write_private(status_file, f'{key}\tNETWORK\n')
|
|
self.assertTrue(keycheck_runner.provider_replay_required(
|
|
'qwen', self.args(retry_network=True), temp_dir,
|
|
))
|
|
with mock.patch.dict(os.environ, {'KEYCHECK_DISABLE_HIGH_WATERMARK': '1'}, clear=False):
|
|
consume()
|
|
self.assertEqual(attempts, [key, key])
|
|
|
|
def test_segmented_force_replay_ignores_every_eof_checkpoint(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, 'findings.jsonl')
|
|
segments = [
|
|
os.path.join(temp_dir, 'findings.000001.jsonl'),
|
|
os.path.join(temp_dir, 'findings.000002.jsonl'),
|
|
]
|
|
for path, row_id in zip(segments + [current], ('one', 'two', 'three')):
|
|
self.write_private(path, json.dumps({'id': row_id}) + '\n')
|
|
keycheck_common.write_jsonl_manifest(current, {
|
|
'current_path': os.path.abspath(current),
|
|
'segments': [
|
|
{'name': os.path.basename(path), 'path': path, 'sequence': index}
|
|
for index, path in enumerate(segments, 1)
|
|
],
|
|
})
|
|
environment = {
|
|
'KEYCHECK_STATE_DIR': state_dir,
|
|
'KEYCHECK_OUTPUT_DIR': state_dir,
|
|
'KEYCHECK_SERVICE': 'fixture',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
}
|
|
|
|
def consume():
|
|
return [item['data']['id'] for item in keycheck_common.iter_jsonl_input(current)]
|
|
|
|
with mock.patch.dict(os.environ, environment, clear=False):
|
|
self.assertEqual(consume(), ['one', 'two', 'three'])
|
|
self.assertEqual(consume(), [])
|
|
with mock.patch.dict(
|
|
os.environ, {'KEYCHECK_DISABLE_HIGH_WATERMARK': '1'}, clear=False,
|
|
):
|
|
self.assertEqual(consume(), ['one', 'two', 'three'])
|
|
|
|
state = json.loads(Path(state_dir, 'input_state.json').read_text(encoding='utf-8'))
|
|
for path in segments + [current]:
|
|
self.assertEqual(state['files'][os.path.abspath(path)]['offset'], os.path.getsize(path))
|
|
|
|
def test_runner_retry_and_recheck_replay_segmented_candidates_to_provider(self):
|
|
replay_modes = (
|
|
('retry-network', {'retry_network': True}, True),
|
|
('recheck-all', {'recheck_all': True}, False),
|
|
)
|
|
for label, overrides, needs_backlog in replay_modes:
|
|
with self.subTest(mode=label), tempfile.TemporaryDirectory() as temp_dir:
|
|
current = os.path.join(temp_dir, 'findings.jsonl')
|
|
segment = os.path.join(temp_dir, 'findings.000001.jsonl')
|
|
keys = ('sk-' + ('w' * 32), 'sk-' + ('x' * 32))
|
|
findings = [
|
|
{
|
|
'DetectorName': 'QwenDashScope',
|
|
'ExtraData': {'name': 'QwenDashScope'},
|
|
'Raw': key,
|
|
}
|
|
for key in keys
|
|
]
|
|
scanner.ensure_private_directory(temp_dir, reject_reparse=True)
|
|
self.write_private(segment, json.dumps(findings[0]) + '\n')
|
|
self.write_private(current, json.dumps(findings[1]) + '\n')
|
|
keycheck_common.write_jsonl_manifest(current, {
|
|
'current_path': os.path.abspath(current),
|
|
'segments': [{
|
|
'name': os.path.basename(segment), 'path': segment, 'sequence': 1,
|
|
}],
|
|
})
|
|
output_dir = os.path.join(temp_dir, 'keychecks', 'qwen')
|
|
scanner.ensure_private_directory(os.path.join(temp_dir, 'keychecks'), reject_reparse=True)
|
|
scanner.ensure_private_directory(output_dir, reject_reparse=True)
|
|
db_path = os.path.join(temp_dir, 'scanner.db')
|
|
ScannerDB(db_path=db_path, db_url='').close()
|
|
reader_environment = {
|
|
'KEYCHECK_STATE_DIR': output_dir,
|
|
'KEYCHECK_OUTPUT_DIR': output_dir,
|
|
'KEYCHECK_SERVICE': 'qwen',
|
|
'KEYCHECK_INPUT_TAIL_BYTES': '0',
|
|
'KEYCHECK_DISABLE_HIGH_WATERMARK': '0',
|
|
'KEYCHECK_DB_URL': '',
|
|
'KEYCHECK_DB_PATH': db_path,
|
|
}
|
|
with mock.patch.dict(os.environ, reader_environment, clear=False):
|
|
self.assertEqual(
|
|
[candidate for candidate, _, _ in qwen.iter_candidate_keys(current, [])],
|
|
list(keys),
|
|
)
|
|
self.assertEqual(list(qwen.iter_candidate_keys(current, [])), [])
|
|
|
|
args = self.args(input=current, **overrides)
|
|
status_text = f'{keys[0]}\tNETWORK\n' if needs_backlog else None
|
|
runner_env = self.captured_runner_env(temp_dir, args, status_text)
|
|
self.assertEqual(runner_env.get('KEYCHECK_DISABLE_HIGH_WATERMARK'), '1')
|
|
response = mock.Mock(status_code=401, text='unauthorized')
|
|
response.json.return_value = {'error': {'message': 'unauthorized'}}
|
|
with mock.patch.dict(os.environ, runner_env, clear=True), mock.patch.object(
|
|
qwen.requests, 'get', return_value=response,
|
|
) as provider_request, mock.patch.object(qwen.requests, 'post') as provider_post:
|
|
candidates = list(qwen.iter_candidate_keys(current, []))
|
|
for candidate, _, _ in candidates:
|
|
qwen.check_key(candidate, [qwen.DEFAULT_BASE_URLS[0]], False, None, 1)
|
|
self.assertEqual([candidate for candidate, _, _ in candidates], list(keys))
|
|
self.assertEqual(provider_request.call_count, 2)
|
|
provider_post.assert_not_called()
|
|
|
|
|
|
class SlowOwnedProcess:
|
|
def __init__(self, started, killed, reaped):
|
|
self.pid = 987654
|
|
self.job_membership_verified = True
|
|
self.payload_identity = {'pid': self.pid}
|
|
self.returncode = None
|
|
self._stopped = False
|
|
self._started = started
|
|
self._killed = killed
|
|
self._reaped = reaped
|
|
self._started.set()
|
|
|
|
def poll(self):
|
|
return -9 if self._stopped else None
|
|
|
|
def kill(self):
|
|
self._stopped = True
|
|
self.returncode = -9
|
|
self._killed.set()
|
|
|
|
def wait(self, timeout=None):
|
|
self._stopped = True
|
|
self.returncode = -9
|
|
self._reaped.set()
|
|
return -9
|
|
|
|
|
|
class FatalScanCancellationTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
|
|
|
|
def setUp(self):
|
|
clear_scan_fatal()
|
|
self.database_environment = mock.patch.dict(os.environ, {
|
|
'SCANNER_DB_URL': '', 'DATABASE_URL': '', 'TRUF_MANAGED_POSTGRES_DSN': '',
|
|
})
|
|
self.database_environment.start()
|
|
|
|
def tearDown(self):
|
|
clear_scan_fatal()
|
|
self.database_environment.stop()
|
|
if hasattr(scanner._scan_slot_scope_local, 'scope'):
|
|
del scanner._scan_slot_scope_local.scope
|
|
|
|
def test_fatal_before_command_workdir_prevents_launch(self):
|
|
scanner._set_scan_slot_fatal('fixture fatal')
|
|
with mock.patch.object(scanner, 'create_command_work_dir') as workdir, \
|
|
mock.patch.object(scanner, 'OwnedProcess') as process:
|
|
with self.assertRaisesRegex(scanner.ScanSlotFatalError, 'fixture fatal'):
|
|
scanner.run_command(['fixture'], 5)
|
|
workdir.assert_not_called()
|
|
process.assert_not_called()
|
|
|
|
def test_download_chunks_stop_on_peer_fatal(self):
|
|
class Response:
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
return False
|
|
|
|
def raise_for_status(self):
|
|
return None
|
|
|
|
def iter_content(self, chunk_size):
|
|
yield b'first'
|
|
scanner._set_scan_slot_fatal('download peer fatal')
|
|
yield b'second'
|
|
|
|
class Session:
|
|
trust_env = True
|
|
|
|
def __enter__(self):
|
|
return self
|
|
|
|
def __exit__(self, *args):
|
|
return False
|
|
|
|
def get(self, *args, **kwargs):
|
|
return Response()
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir, \
|
|
mock.patch.object(scanner.requests, 'Session', return_value=Session()):
|
|
path = os.path.join(temp_dir, 'artifact.bin')
|
|
with self.assertRaisesRegex(scanner.ScanSlotFatalError, 'download peer fatal'):
|
|
scanner.download_file('https://example.invalid/artifact', path, timeout=1)
|
|
self.assertEqual(Path(path).read_bytes(), b'first')
|
|
|
|
def test_completed_network_call_cannot_launch_after_peer_fatal(self):
|
|
request_started = threading.Event()
|
|
release_request = threading.Event()
|
|
response = mock.Mock(status_code=200, headers={}, text='')
|
|
|
|
def request(*args, **kwargs):
|
|
request_started.set()
|
|
if not release_request.wait(2):
|
|
raise AssertionError('test did not release network request')
|
|
return response
|
|
|
|
def scan_target(target, provider=None, **kwargs):
|
|
if target == 'fatal':
|
|
if not request_started.wait(2):
|
|
raise AssertionError('network request did not start')
|
|
scanner._set_scan_slot_fatal('network peer fatal')
|
|
release_request.set()
|
|
raise scanner.ScanSlotFatalError('network peer fatal')
|
|
scanner.api_request('GET', 'https://example.invalid', timeout=1)
|
|
scanner.run_command(['must-not-launch'], 5)
|
|
return {'findings': [], 'errors': []}
|
|
|
|
with mock.patch.object(scanner.scan_config, 'max_active_scans', 0), \
|
|
mock.patch.object(scanner, 'scan_git_repo', side_effect=scan_target), \
|
|
mock.patch.object(scanner.requests, 'request', side_effect=request), \
|
|
mock.patch.object(scanner, 'OwnedProcess') as process:
|
|
with self.assertRaisesRegex(scanner.ScanSlotFatalError, 'network peer fatal'):
|
|
scanner.scan_targets_batch(
|
|
['network', 'fatal'], 'git', max_workers=2, persist_results=False,
|
|
)
|
|
process.assert_not_called()
|
|
response.close.assert_called_once()
|
|
|
|
def test_batch_fatal_kills_owned_child_and_never_launches_queued_target(self):
|
|
for max_active in (0, 2):
|
|
with self.subTest(max_active=max_active), tempfile.TemporaryDirectory() as temp_dir:
|
|
clear_scan_fatal()
|
|
started = threading.Event()
|
|
killed = threading.Event()
|
|
reaped = threading.Event()
|
|
process = SlowOwnedProcess(started, killed, reaped)
|
|
process._started.clear()
|
|
limiter_path = os.path.join(temp_dir, 'limiter.sqlite')
|
|
|
|
def owned_process(*args, **kwargs):
|
|
process._started.set()
|
|
return process
|
|
|
|
def scan_target(target, provider=None, **kwargs):
|
|
if target == 'fatal':
|
|
if not started.wait(2):
|
|
raise AssertionError('slow child did not launch')
|
|
scanner._set_scan_slot_fatal('peer fatal')
|
|
raise scanner.ScanSlotFatalError('peer fatal')
|
|
if target == 'slow':
|
|
scanner.run_command(['fixture', target], 30)
|
|
else:
|
|
scanner.run_command(['fixture', target], 30)
|
|
return {'findings': [], 'errors': []}
|
|
|
|
with mock.patch.object(scanner.scan_config, 'max_active_scans', max_active), \
|
|
mock.patch.object(scanner.scan_config, 'scan_limiter_db', limiter_path), \
|
|
mock.patch.object(scanner.scan_config, 'scan_slot_wait_sec', 0.01), \
|
|
mock.patch.object(scanner.scan_config, 'min_free_gb', 0), \
|
|
mock.patch.object(scanner.scan_config, 'trufflehog_job_memory_limit_bytes', 1024 * 1024), \
|
|
mock.patch.object(scanner, 'scan_git_repo', side_effect=scan_target), \
|
|
mock.patch.object(scanner, 'create_command_work_dir', return_value=temp_dir), \
|
|
mock.patch.object(scanner, 'cleanup_command_work_dir'), \
|
|
mock.patch.object(scanner, '_shared_staging_owners', return_value=[]), \
|
|
mock.patch.object(scanner, 'require_trufflehog_launch_authority'), \
|
|
mock.patch.object(scanner, 'write_temp_owner'), \
|
|
mock.patch.object(scanner, 'OwnedProcess', side_effect=owned_process) as launches:
|
|
began = time.monotonic()
|
|
with self.assertRaisesRegex(scanner.ScanSlotFatalError, 'peer fatal'):
|
|
scanner.scan_targets_batch(
|
|
['slow', 'fatal', 'queued'], 'git', max_workers=2, persist_results=False,
|
|
)
|
|
elapsed = time.monotonic() - began
|
|
|
|
self.assertLess(elapsed, 3)
|
|
self.assertTrue(killed.wait(1))
|
|
self.assertTrue(reaped.wait(1))
|
|
self.assertEqual(launches.call_count, 1)
|
|
if max_active:
|
|
conn = sqlite3.connect(limiter_path)
|
|
try:
|
|
deadline = time.monotonic() + 2
|
|
while conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0] and time.monotonic() < deadline:
|
|
time.sleep(0.05)
|
|
self.assertEqual(conn.execute('SELECT COUNT(*) FROM scan_slots').fetchone()[0], 0)
|
|
finally:
|
|
conn.close()
|
|
deadline = time.monotonic() + 3
|
|
while any(
|
|
thread.is_alive() and thread.name.startswith('scan-slot-heartbeat-')
|
|
for thread in threading.enumerate()
|
|
) and time.monotonic() < deadline:
|
|
time.sleep(0.05)
|
|
|
|
|
|
class DockerTagIdentityTests(unittest.TestCase):
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
scanner.initialize_scanner_runtime(preflight_complete=True, register_cleanup=False)
|
|
|
|
def setUp(self):
|
|
clear_scan_fatal()
|
|
self.database_environment = mock.patch.dict(os.environ, {
|
|
'SCANNER_DB_URL': '', 'DATABASE_URL': '', 'TRUF_MANAGED_POSTGRES_DSN': '',
|
|
})
|
|
self.database_environment.start()
|
|
|
|
def tearDown(self):
|
|
clear_scan_fatal()
|
|
self.database_environment.stop()
|
|
|
|
@staticmethod
|
|
def response(payload):
|
|
response = mock.Mock(status_code=200, headers={}, text='')
|
|
response.json.return_value = payload
|
|
response.raise_for_status.return_value = None
|
|
return response
|
|
|
|
def fetch_tags(self, *tags, **kwargs):
|
|
def resolve_graph(_repo_name, digest, _platform_os, _platform_arch, bearer_token):
|
|
return {
|
|
'manifest_digest': digest,
|
|
'layers': (digest,),
|
|
}, bearer_token or 'fixture-token'
|
|
|
|
with mock.patch.object(scanner, 'get_dockerhub_tag_cache', return_value=None), \
|
|
mock.patch.object(
|
|
scanner, 'dockerhub_tag_rate_limit_state',
|
|
return_value={'active': False, 'retry_at': None},
|
|
), \
|
|
mock.patch.object(scanner, 'put_dockerhub_tag_cache') as cache, \
|
|
mock.patch.object(scanner, 'resolve_docker_layer_graph', side_effect=resolve_graph), \
|
|
mock.patch.object(scanner, 'api_request', return_value=self.response({'results': list(tags)})):
|
|
targets, status = scanner.fetch_dockerhub_tags(
|
|
'owner/repo', retry_count=0, return_status=True, **kwargs,
|
|
)
|
|
return targets, status, cache
|
|
|
|
def test_digest_repoint_changes_queue_identity(self):
|
|
digest_one = 'sha256:' + ('1' * 64)
|
|
digest_two = 'sha256:' + ('2' * 64)
|
|
first, status, _ = self.fetch_tags({'name': 'latest', 'digest': digest_one, 'last_updated': '2026-07-19T00:00:00Z'})
|
|
self.assertEqual(status, 'ok')
|
|
same, _, _ = self.fetch_tags({'name': 'latest', 'digest': digest_one, 'last_updated': '2026-07-20T00:00:00Z'})
|
|
repointed, _, _ = self.fetch_tags({'name': 'latest', 'digest': digest_two, 'last_updated': '2026-07-20T00:00:00Z'})
|
|
first = first[0]
|
|
same = same[0]
|
|
repointed = repointed[0]
|
|
self.assertEqual(first, f'owner/repo@{digest_one}')
|
|
self.assertEqual(normalize_target(first, 'docker'), normalize_target(same, 'docker'))
|
|
self.assertNotEqual(normalize_target(first, 'docker'), normalize_target(repointed, 'docker'))
|
|
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [first, same, repointed])
|
|
count = db.conn.execute(
|
|
"SELECT COUNT(*) AS n FROM target_queue WHERE source = 'dockerhub'"
|
|
).fetchone()['n']
|
|
self.assertEqual(count, 2)
|
|
finally:
|
|
db.close()
|
|
|
|
def test_no_digest_discovery_is_unknown_and_not_cached(self):
|
|
targets, status, cache = self.fetch_tags({
|
|
'name': 'latest',
|
|
'last_updated': '2026-07-20T01:02:03Z',
|
|
'images': [{'os': 'linux', 'architecture': 'amd64'}],
|
|
})
|
|
self.assertEqual((targets, status), ([], 'unknown'))
|
|
cache.assert_not_called()
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets(
|
|
'dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'],
|
|
)
|
|
row = db.conn.execute(
|
|
"SELECT status, resolver_state FROM target_queue WHERE source = 'dockerhub'"
|
|
).fetchone()
|
|
self.assertEqual((row['status'], row['resolver_state']), ('deferred', 'pending'))
|
|
finally:
|
|
db.close()
|
|
|
|
def test_resolved_bare_repository_rediscovery_preserves_periodic_refresh(self):
|
|
digest_a = 'sha256:' + ('a' * 64)
|
|
digest_b = 'sha256:' + ('b' * 64)
|
|
with tempfile.TemporaryDirectory() as temp_dir, mock.patch.dict(os.environ, {
|
|
'DOCKER_RESOLVER_RETRY_SEC': '60',
|
|
}):
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00' "
|
|
"WHERE source = 'dockerhub' AND target = 'owner/repo'"
|
|
)
|
|
db.conn.commit()
|
|
first = db.claim_docker_resolutions('dockerhub', 1, 'resolver-a', 60)[0]
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', first['id'], first['resolver_token'],
|
|
[f'owner/repo@{digest_a}'],
|
|
refresh_interval_sec=86400,
|
|
))
|
|
|
|
resolved = db.conn.execute(
|
|
'''SELECT status, resolver_state, resolver_due_at, available_after,
|
|
resolver_attempts, resolver_token, completed_at
|
|
FROM target_queue WHERE id = ?''',
|
|
(first['id'],),
|
|
).fetchone()
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
rediscovered = db.conn.execute(
|
|
'''SELECT status, resolver_state, resolver_due_at, available_after,
|
|
resolver_attempts, resolver_token, completed_at
|
|
FROM target_queue WHERE id = ?''',
|
|
(first['id'],),
|
|
).fetchone()
|
|
self.assertEqual(dict(rediscovered), dict(resolved))
|
|
self.assertEqual(db.claim_docker_resolutions(
|
|
'dockerhub', 1, 'resolver-early', 60, periodic_limit=1,
|
|
), [])
|
|
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00' "
|
|
"WHERE id = ?",
|
|
(first['id'],),
|
|
)
|
|
db.conn.commit()
|
|
second = db.claim_docker_resolutions(
|
|
'dockerhub', 1, 'resolver-b', 60, periodic_limit=1, periodic_only=True,
|
|
)[0]
|
|
self.assertTrue(second['periodic'])
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', second['id'], second['resolver_token'],
|
|
[f'owner/repo@{digest_b}'],
|
|
))
|
|
targets = {
|
|
row['target'] for row in db.conn.execute(
|
|
"SELECT target FROM target_queue WHERE source = 'dockerhub'"
|
|
).fetchall()
|
|
}
|
|
self.assertEqual(targets, {
|
|
'owner/repo', f'owner/repo@{digest_a}', f'owner/repo@{digest_b}',
|
|
})
|
|
finally:
|
|
db.close()
|
|
|
|
def test_rediscovery_preserves_active_resolver_and_scan_leases(self):
|
|
digest = 'sha256:' + ('c' * 64)
|
|
digest_target = f'owner/active@{digest}'
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00' "
|
|
"WHERE source = 'dockerhub' AND target = 'owner/repo'"
|
|
)
|
|
db.conn.commit()
|
|
resolving = db.claim_docker_resolutions('dockerhub', 1, 'resolver', 60)[0]
|
|
active = db.conn.execute(
|
|
'SELECT resolver_due_at, resolver_token, resolver_attempts FROM target_queue WHERE id = ?',
|
|
(resolving['id'],),
|
|
).fetchone()
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
preserved = db.conn.execute(
|
|
'''SELECT resolver_state, resolver_due_at, resolver_token, resolver_attempts
|
|
FROM target_queue WHERE id = ?''',
|
|
(resolving['id'],),
|
|
).fetchone()
|
|
self.assertEqual(preserved['resolver_state'], 'resolving')
|
|
self.assertEqual(dict(preserved), {
|
|
'resolver_state': 'resolving',
|
|
'resolver_due_at': active['resolver_due_at'],
|
|
'resolver_token': active['resolver_token'],
|
|
'resolver_attempts': active['resolver_attempts'],
|
|
})
|
|
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00' WHERE id = ?",
|
|
(resolving['id'],),
|
|
)
|
|
db.conn.commit()
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
expired = db.conn.execute(
|
|
'SELECT status, resolver_state, resolver_token FROM target_queue WHERE id = ?',
|
|
(resolving['id'],),
|
|
).fetchone()
|
|
self.assertEqual((expired['status'], expired['resolver_state']), ('deferred', 'pending'))
|
|
self.assertIsNone(expired['resolver_token'])
|
|
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [digest_target])
|
|
scan = db.claim_targets(
|
|
'dockerhub', 'docker', 1, 'scanner', 60, max_attempts=3, return_rows=True,
|
|
)[0]
|
|
db.enqueue_targets(
|
|
'dockerhub', 'docker', 'q', [], unresolved_targets=[digest_target],
|
|
)
|
|
scan_after = db.conn.execute(
|
|
'SELECT status, lease_token, resolver_state FROM target_queue WHERE id = ?',
|
|
(scan['id'],),
|
|
).fetchone()
|
|
self.assertEqual(scan_after['status'], 'in_progress')
|
|
self.assertEqual(scan_after['lease_token'], scan['lease_token'])
|
|
self.assertIsNone(scan_after['resolver_state'])
|
|
finally:
|
|
db.close()
|
|
|
|
def test_rediscovered_unchanged_digest_is_deduplicated(self):
|
|
digest = 'sha256:' + ('d' * 64)
|
|
target = f'owner/repo@{digest}'
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
for owner in ('resolver-a', 'resolver-b'):
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00' "
|
|
"WHERE source = 'dockerhub' AND target = 'owner/repo'"
|
|
)
|
|
db.conn.commit()
|
|
row = db.claim_docker_resolutions(
|
|
'dockerhub', 1, owner, 60, periodic_limit=1,
|
|
)[0]
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', row['id'], row['resolver_token'], [target],
|
|
))
|
|
db.enqueue_targets(
|
|
'dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'],
|
|
)
|
|
count = db.conn.execute(
|
|
'SELECT COUNT(*) AS n FROM target_queue WHERE source = ?', ('dockerhub',),
|
|
).fetchone()['n']
|
|
self.assertEqual(count, 2)
|
|
finally:
|
|
db.close()
|
|
|
|
def test_matching_platform_digest_is_pinned(self):
|
|
digest = 'sha256:' + ('3' * 64)
|
|
targets, status, _ = self.fetch_tags({
|
|
'name': 'latest',
|
|
'images': [
|
|
{'os': 'linux', 'architecture': 'arm64', 'digest': 'sha256:' + ('4' * 64)},
|
|
{'os': 'linux', 'architecture': 'amd64', 'digest': digest},
|
|
],
|
|
}, platform_filter_enabled=True)
|
|
self.assertEqual((targets, status), ([f'owner/repo@{digest}'], 'ok'))
|
|
|
|
def test_layer_graph_selector_deduplicates_aliases_and_fills_three_roles(self):
|
|
layer = lambda value: 'sha256:' + (value * 64)
|
|
candidates = [
|
|
{'target': 'newest', 'updated_at': 40, 'source_index': 0,
|
|
'layers': (layer('a'), layer('b'), layer('c'))},
|
|
{'target': 'newest-alias', 'updated_at': 30, 'source_index': 1,
|
|
'layers': (layer('a'), layer('b'), layer('c'))},
|
|
{'target': 'recent-small-change', 'updated_at': 35, 'source_index': 2,
|
|
'layers': (layer('a'), layer('b'), layer('d'))},
|
|
{'target': 'maximum-novelty', 'updated_at': 20, 'source_index': 3,
|
|
'layers': (layer('a'), layer('e'), layer('f'))},
|
|
{'target': 'oldest-reordered', 'updated_at': 10, 'source_index': 4,
|
|
'layers': (layer('c'), layer('b'), layer('a'))},
|
|
]
|
|
selected = scanner.select_docker_layer_graphs(candidates, 3)
|
|
self.assertEqual(
|
|
[item['target'] for item in selected],
|
|
['newest', 'maximum-novelty', 'oldest-reordered'],
|
|
)
|
|
for invalid in (False, True, None, '3', 3.0, 0, 11):
|
|
with self.subTest(invalid=invalid), self.assertRaises(ValueError):
|
|
scanner.docker_images_per_repository_limit(invalid)
|
|
|
|
def test_registry_resolves_requested_child_and_ordered_layers_with_bearer_auth(self):
|
|
index_digest = 'sha256:' + ('1' * 64)
|
|
child_digest = 'sha256:' + ('2' * 64)
|
|
arm_digest = 'sha256:' + ('3' * 64)
|
|
layers = ['sha256:' + ('a' * 64), 'sha256:' + ('b' * 64)]
|
|
|
|
def registry_response(status, payload, headers=None):
|
|
response = mock.Mock(status_code=status, headers=headers or {}, content=b'{}')
|
|
response.json.return_value = payload
|
|
response.raise_for_status.return_value = None
|
|
return response
|
|
|
|
responses = [
|
|
registry_response(401, {}, {
|
|
'WWW-Authenticate': (
|
|
'Bearer realm="https://auth.docker.io/token",'
|
|
'service="registry.docker.io",scope="repository:owner/repo:pull"'
|
|
),
|
|
}),
|
|
registry_response(200, {'token': 'registry-token'}),
|
|
registry_response(200, {
|
|
'schemaVersion': 2,
|
|
'manifests': [
|
|
{'digest': arm_digest, 'platform': {'os': 'linux', 'architecture': 'arm64'}},
|
|
{'digest': child_digest, 'platform': {'os': 'linux', 'architecture': 'amd64'}},
|
|
],
|
|
}, {'Docker-Content-Digest': index_digest}),
|
|
registry_response(200, {
|
|
'schemaVersion': 2,
|
|
'layers': [{'digest': digest} for digest in layers],
|
|
}, {'Docker-Content-Digest': child_digest}),
|
|
]
|
|
manager = scanner.DockerTokenManager()
|
|
manager.accounts = [
|
|
scanner.DockerAccount('account-a', 'user-a', 'secret-a', ''),
|
|
]
|
|
manager.explicit_pool = True
|
|
with mock.patch.object(scanner, 'docker_token_manager', manager), \
|
|
mock.patch.object(scanner, 'api_request', side_effect=responses) as request:
|
|
graph, token = scanner.resolve_docker_layer_graph('owner/repo', index_digest)
|
|
self.assertEqual(graph, {
|
|
'manifest_digest': child_digest,
|
|
'layers': tuple(layers),
|
|
})
|
|
self.assertEqual(token.token, 'registry-token')
|
|
self.assertEqual(token.account_name, 'account-a')
|
|
self.assertEqual(request.call_count, 4)
|
|
self.assertEqual(request.call_args_list[1].kwargs['auth'], ('user-a', 'secret-a'))
|
|
self.assertEqual(request.call_args_list[1].args[1], 'https://auth.docker.io/token')
|
|
child_headers = request.call_args_list[-1].kwargs['headers']
|
|
self.assertEqual(child_headers['Authorization'], 'Bearer registry-token')
|
|
|
|
def test_registry_rejects_oversized_and_invalid_layer_manifests(self):
|
|
oversized = self.response({'token': 'ignored'})
|
|
oversized.headers = {
|
|
'Content-Length': str(scanner.DOCKER_REGISTRY_MANIFEST_MAX_BYTES + 1),
|
|
}
|
|
with self.assertRaisesRegex(scanner.DockerRegistryResolutionError, 'size limit'):
|
|
scanner._bounded_docker_registry_json(oversized, 'fixture')
|
|
|
|
digest = 'sha256:' + ('9' * 64)
|
|
with mock.patch.object(
|
|
scanner, 'docker_registry_manifest',
|
|
return_value=({'layers': [{'digest': 'not-a-digest'}]}, 'token'),
|
|
):
|
|
with self.assertRaisesRegex(scanner.DockerRegistryResolutionError, 'layer digest'):
|
|
scanner.resolve_docker_layer_graph('owner/repo', digest)
|
|
|
|
def test_search_resolution_plumbs_ten_image_budget(self):
|
|
response = self.response({
|
|
'count': 1,
|
|
'results': [{'repo_name': 'owner/repo'}],
|
|
})
|
|
digest = 'sha256:' + ('8' * 64)
|
|
with mock.patch.object(scanner, 'api_request', return_value=response), \
|
|
mock.patch.object(
|
|
scanner, 'fetch_dockerhub_tags',
|
|
return_value=([f'owner/repo@{digest}'], 'ok'),
|
|
) as fetch_tags:
|
|
targets = scanner.fetch_dockerhub_images(
|
|
'fixture', pages=1, images_per_repository=10,
|
|
)
|
|
self.assertEqual(targets, [f'owner/repo@{digest}'])
|
|
self.assertEqual(fetch_tags.call_args.args[2], 10)
|
|
|
|
def test_partial_graph_resolution_emits_subset_without_caching_it(self):
|
|
good = 'sha256:' + ('4' * 64)
|
|
failed = 'sha256:' + ('5' * 64)
|
|
tags = [
|
|
{'name': 'good', 'digest': good, 'last_updated': '2026-07-20T02:00:00Z'},
|
|
{'name': 'failed', 'digest': failed, 'last_updated': '2026-07-20T01:00:00Z'},
|
|
]
|
|
|
|
def resolve(_repo_name, digest, *_args):
|
|
if digest == failed:
|
|
raise scanner.DockerRegistryResolutionError('temporary manifest failure')
|
|
return {'manifest_digest': digest, 'layers': (digest,)}, 'token'
|
|
|
|
with mock.patch.object(scanner, 'get_dockerhub_tag_cache', return_value=None), \
|
|
mock.patch.object(
|
|
scanner, 'dockerhub_tag_rate_limit_state',
|
|
return_value={'active': False, 'retry_at': None},
|
|
), \
|
|
mock.patch.object(scanner, 'put_dockerhub_tag_cache') as cache, \
|
|
mock.patch.object(scanner, 'resolve_docker_layer_graph', side_effect=resolve), \
|
|
mock.patch.object(scanner, 'api_request', return_value=self.response({'results': tags})):
|
|
targets, status = scanner.fetch_dockerhub_tags(
|
|
'owner/repo', limit=3, retry_count=0, return_status=True,
|
|
)
|
|
self.assertEqual((targets, status), ([f'owner/repo@{good}'], 'partial'))
|
|
cache.assert_not_called()
|
|
|
|
def test_partial_queue_resolution_keeps_bare_repository_retryable(self):
|
|
digest = 'sha256:' + ('6' * 64)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00'"
|
|
)
|
|
db.conn.commit()
|
|
claim = db.claim_docker_resolutions('dockerhub', 1, 'resolver', 60)[0]
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', claim['id'], claim['resolver_token'],
|
|
[f'owner/repo@{digest}'], 'partial manifest resolution', complete=False,
|
|
))
|
|
bare = db.conn.execute(
|
|
'SELECT status, resolver_state, resolver_due_at FROM target_queue WHERE id = ?',
|
|
(claim['id'],),
|
|
).fetchone()
|
|
self.assertEqual((bare['status'], bare['resolver_state']), ('deferred', 'retry'))
|
|
self.assertTrue(bare['resolver_due_at'])
|
|
emitted = db.target_queue_item('dockerhub', 'docker', f'owner/repo@{digest}')
|
|
self.assertEqual(emitted['status'], 'pending')
|
|
finally:
|
|
db.close()
|
|
|
|
def test_global_cooldown_refunds_claim_attempt_and_stale_token_cannot_emit(self):
|
|
digest = 'sha256:' + ('7' * 64)
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/retry'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_attempts = 7, "
|
|
"resolver_due_at = '2000-01-01T00:00:00+00:00'"
|
|
)
|
|
db.conn.commit()
|
|
claim = db.claim_docker_resolutions('dockerhub', 1, 'resolver', 60)[0]
|
|
self.assertEqual(claim['resolver_attempts_before'], 7)
|
|
self.assertEqual(claim['resolver_attempts'], 8)
|
|
retry_at = datetime.now(timezone.utc) + timedelta(minutes=5)
|
|
retry_at = retry_at.isoformat(timespec='seconds')
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', claim['id'], claim['resolver_token'], [],
|
|
'shared cooldown', complete=False, retry_at=retry_at,
|
|
claim_attempt_consumed=False,
|
|
))
|
|
row = db.conn.execute(
|
|
'''SELECT status, resolver_state, resolver_attempts,
|
|
resolver_due_at, available_after
|
|
FROM target_queue WHERE id = ?''',
|
|
(claim['id'],),
|
|
).fetchone()
|
|
self.assertEqual((row['status'], row['resolver_state']), ('deferred', 'retry'))
|
|
self.assertEqual(row['resolver_attempts'], 7)
|
|
self.assertEqual(row['resolver_due_at'], retry_at)
|
|
self.assertEqual(row['available_after'], retry_at)
|
|
self.assertFalse(db.finish_docker_resolution(
|
|
'dockerhub', claim['id'], claim['resolver_token'],
|
|
[f'owner/retry@{digest}'], complete=True,
|
|
))
|
|
self.assertIsNone(db.target_queue_item(
|
|
'dockerhub', 'docker', f'owner/retry@{digest}',
|
|
))
|
|
finally:
|
|
db.close()
|
|
|
|
def test_periodic_repository_refresh_has_reserved_lane_and_deduplicates_digests(self):
|
|
digest_a = 'sha256:' + ('8' * 64)
|
|
digest_b = 'sha256:' + ('9' * 64)
|
|
target_a = f'owner/repo@{digest_a}'
|
|
target_b = f'owner/repo@{digest_b}'
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
db = ScannerDB(db_path=os.path.join(temp_dir, 'queue.db'))
|
|
try:
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/repo'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00'"
|
|
)
|
|
db.conn.commit()
|
|
first = db.claim_docker_resolutions('dockerhub', 1, 'initial', 60)[0]
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', first['id'], first['resolver_token'], [target_a],
|
|
refresh_interval_sec=86400,
|
|
))
|
|
anchor = db.conn.execute(
|
|
'''SELECT status, resolver_state, resolver_attempts, resolver_due_at
|
|
FROM target_queue WHERE id = ?''',
|
|
(first['id'],),
|
|
).fetchone()
|
|
self.assertEqual((anchor['status'], anchor['resolver_state']), ('done', 'resolved'))
|
|
self.assertEqual(anchor['resolver_attempts'], 0)
|
|
self.assertTrue(anchor['resolver_due_at'])
|
|
|
|
db.enqueue_targets('dockerhub', 'docker', 'q', [], unresolved_targets=['owner/retry'])
|
|
db.conn.execute(
|
|
"UPDATE target_queue SET resolver_due_at = '2000-01-01T00:00:00+00:00', "
|
|
"available_after = '2000-01-01T00:00:00+00:00' "
|
|
"WHERE target IN ('owner/repo', 'owner/retry')"
|
|
)
|
|
db.conn.commit()
|
|
retry = db.claim_docker_resolutions(
|
|
'dockerhub', 1, 'retry-first', 60, periodic_limit=1,
|
|
)[0]
|
|
self.assertEqual(retry['target'], 'owner/retry')
|
|
self.assertFalse(retry['periodic'])
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', retry['id'], retry['resolver_token'], [],
|
|
complete=True, refresh_interval_sec=86400,
|
|
))
|
|
|
|
periodic = db.claim_docker_resolutions(
|
|
'dockerhub', 1, 'periodic', 60,
|
|
periodic_limit=1, periodic_only=True,
|
|
)[0]
|
|
self.assertEqual(periodic['target'], 'owner/repo')
|
|
self.assertTrue(periodic['periodic'])
|
|
self.assertFalse(db.finish_docker_resolution(
|
|
'dockerhub', periodic['id'], 'stale-token', [target_b],
|
|
refresh_interval_sec=86400,
|
|
))
|
|
self.assertIsNone(db.target_queue_item('dockerhub', 'docker', target_b))
|
|
self.assertTrue(db.finish_docker_resolution(
|
|
'dockerhub', periodic['id'], periodic['resolver_token'],
|
|
[target_a, target_b], refresh_interval_sec=86400,
|
|
))
|
|
rows = db.conn.execute(
|
|
"SELECT target, status FROM target_queue WHERE source = 'dockerhub' ORDER BY target"
|
|
).fetchall()
|
|
self.assertEqual(sum(row['target'] == target_a for row in rows), 1)
|
|
self.assertEqual(sum(row['target'] == target_b for row in rows), 1)
|
|
self.assertEqual(
|
|
db.target_queue_item('dockerhub', 'docker', 'owner/repo')['status'], 'done',
|
|
)
|
|
finally:
|
|
db.close()
|
|
|
|
def test_due_resolver_stops_after_shared_cooldown_without_preclaiming_batch(self):
|
|
retry_at = (datetime.now(timezone.utc) + timedelta(minutes=5)).isoformat(timespec='seconds')
|
|
db = mock.Mock()
|
|
db.runtime_control_state.return_value = {
|
|
'effective_discovery_paused': False,
|
|
}
|
|
db.claim_docker_resolutions.return_value = [{
|
|
'id': 11, 'target': 'owner/repo', 'resolver_token': 'lease', 'periodic': False,
|
|
}]
|
|
db.finish_docker_resolution.return_value = True
|
|
args = SimpleNamespace(
|
|
tag_resolve_limit=100,
|
|
docker_repository_refresh_max_per_cycle=1,
|
|
docker_repository_refresh_interval_sec=86400,
|
|
docker_images_per_repository=3,
|
|
tag_retry_count=0,
|
|
tag_retry_delay=0,
|
|
docker_platform_filter_enabled=True,
|
|
docker_platform_os='linux',
|
|
docker_platform_arch='amd64',
|
|
docker_platform_candidate_tags=20,
|
|
)
|
|
outcome = scanner.DockerTagResolutionOutcome(
|
|
tags=(), status='global_cooldown', remote_attempted=False,
|
|
retry_at=retry_at, error='shared cooldown',
|
|
)
|
|
with mock.patch.object(console_runner, 'fetch_dockerhub_tags', return_value=outcome):
|
|
console_runner.resolve_due_docker_queue_targets(db, 'dockerhub', args)
|
|
self.assertEqual(db.claim_docker_resolutions.call_count, 1)
|
|
finish = db.finish_docker_resolution.call_args
|
|
self.assertEqual(finish.kwargs['retry_at'], retry_at)
|
|
self.assertFalse(finish.kwargs['claim_attempt_consumed'])
|
|
|
|
def test_due_resolver_persists_resolved_immutable_targets_without_access_probe(self):
|
|
target = 'docker.io/acme/public@sha256:' + ('a' * 64)
|
|
db = mock.Mock()
|
|
db.runtime_control_state.return_value = {
|
|
'effective_discovery_paused': False,
|
|
}
|
|
db.claim_docker_resolutions.side_effect = [[{
|
|
'id': 21, 'target': 'acme/public', 'resolver_token': 'lease',
|
|
'periodic': False,
|
|
}], []]
|
|
db.finish_docker_resolution.return_value = True
|
|
args = SimpleNamespace(
|
|
tag_resolve_limit=1, docker_repository_refresh_max_per_cycle=0,
|
|
docker_repository_refresh_interval_sec=86400,
|
|
docker_images_per_repository=1, tag_retry_count=0,
|
|
tag_retry_delay=0, docker_platform_filter_enabled=True,
|
|
docker_platform_os='linux', docker_platform_arch='amd64',
|
|
docker_platform_candidate_tags=20,
|
|
)
|
|
outcome = scanner.DockerTagResolutionOutcome(
|
|
tags=(target,), status='ok', remote_attempted=True,
|
|
)
|
|
with mock.patch.object(
|
|
console_runner, 'fetch_dockerhub_tags', return_value=outcome,
|
|
):
|
|
console_runner.resolve_due_docker_queue_targets(db, 'dockerhub', args)
|
|
|
|
finish = db.finish_docker_resolution.call_args
|
|
self.assertEqual(finish.args[3], [target])
|
|
self.assertNotIn('anonymous_verified_targets', finish.kwargs)
|
|
self.assertNotIn('anonymous_verification_complete', finish.kwargs)
|
|
self.assertTrue(finish.kwargs['complete'])
|
|
|
|
def test_due_resolver_stops_after_auth_failure_and_consumes_only_one_claim(self):
|
|
db = mock.Mock()
|
|
db.runtime_control_state.return_value = {
|
|
'effective_discovery_paused': False,
|
|
}
|
|
db.claim_docker_resolutions.return_value = [{
|
|
'id': 12, 'target': 'owner/repo', 'resolver_token': 'lease', 'periodic': False,
|
|
}]
|
|
db.finish_docker_resolution.return_value = True
|
|
args = SimpleNamespace(
|
|
tag_resolve_limit=100,
|
|
docker_repository_refresh_max_per_cycle=1,
|
|
docker_repository_refresh_interval_sec=86400,
|
|
docker_images_per_repository=3,
|
|
tag_retry_count=0,
|
|
tag_retry_delay=0,
|
|
docker_platform_filter_enabled=True,
|
|
docker_platform_os='linux',
|
|
docker_platform_arch='amd64',
|
|
docker_platform_candidate_tags=20,
|
|
)
|
|
outcome = scanner.DockerTagResolutionOutcome(
|
|
tags=(), status='auth_failed', remote_attempted=False,
|
|
error='Docker authentication is unavailable',
|
|
)
|
|
with mock.patch.object(console_runner, 'fetch_dockerhub_tags', return_value=outcome):
|
|
console_runner.resolve_due_docker_queue_targets(db, 'dockerhub', args)
|
|
self.assertEqual(db.claim_docker_resolutions.call_count, 1)
|
|
finish = db.finish_docker_resolution.call_args
|
|
self.assertTrue(finish.kwargs['claim_attempt_consumed'])
|
|
self.assertIsNone(finish.kwargs['retry_at'])
|
|
|
|
def test_due_resolver_reserves_one_periodic_lane_inside_total_limit(self):
|
|
retry_rows = [
|
|
[{'id': 1, 'target': 'owner/a', 'resolver_token': 'a', 'periodic': False}],
|
|
[{'id': 2, 'target': 'owner/b', 'resolver_token': 'b', 'periodic': False}],
|
|
[{'id': 3, 'target': 'owner/c', 'resolver_token': 'c', 'periodic': True}],
|
|
]
|
|
db = mock.Mock()
|
|
db.claim_docker_resolutions.side_effect = retry_rows
|
|
db.finish_docker_resolution.return_value = True
|
|
args = SimpleNamespace(
|
|
tag_resolve_limit=3,
|
|
docker_repository_refresh_max_per_cycle=1,
|
|
docker_repository_refresh_interval_sec=86400,
|
|
docker_images_per_repository=3,
|
|
tag_retry_count=0,
|
|
tag_retry_delay=0,
|
|
docker_platform_filter_enabled=True,
|
|
docker_platform_os='linux',
|
|
docker_platform_arch='amd64',
|
|
docker_platform_candidate_tags=20,
|
|
)
|
|
outcome = scanner.DockerTagResolutionOutcome(
|
|
tags=(), status='empty', remote_attempted=True,
|
|
)
|
|
with mock.patch.object(console_runner, 'fetch_dockerhub_tags', return_value=outcome):
|
|
console_runner.resolve_due_docker_queue_targets(db, 'dockerhub', args)
|
|
self.assertEqual(db.claim_docker_resolutions.call_count, 3)
|
|
self.assertNotIn('periodic_only', db.claim_docker_resolutions.call_args_list[0].kwargs)
|
|
self.assertNotIn('periodic_only', db.claim_docker_resolutions.call_args_list[1].kwargs)
|
|
self.assertIs(db.claim_docker_resolutions.call_args_list[2].kwargs['periodic_only'], True)
|
|
self.assertEqual(db.finish_docker_resolution.call_count, 3)
|
|
|
|
def test_due_resolver_yields_to_claimable_immutable_scan_backlog(self):
|
|
db = mock.Mock(last_error=None)
|
|
db.has_claimable_targets_v2.return_value = True
|
|
args = SimpleNamespace(target_retry_max_attempts=3)
|
|
with mock.patch.object(console_runner, 'resolve_due_docker_queue_targets') as resolve:
|
|
processed = console_runner.resolve_due_docker_queue_targets_if_scan_queue_empty(
|
|
db, 'dockerhub', args,
|
|
)
|
|
self.assertEqual(processed, 0)
|
|
db.has_claimable_targets_v2.assert_called_once_with(
|
|
'dockerhub', 'docker', max_attempts=3,
|
|
)
|
|
resolve.assert_not_called()
|
|
|
|
db.has_claimable_targets_v2.return_value = False
|
|
with mock.patch.object(
|
|
console_runner, 'resolve_due_docker_queue_targets', return_value=7,
|
|
) as resolve:
|
|
processed = console_runner.resolve_due_docker_queue_targets_if_scan_queue_empty(
|
|
db, 'dockerhub', args,
|
|
)
|
|
self.assertEqual(processed, 7)
|
|
resolve.assert_called_once_with(db, 'dockerhub', args)
|
|
|
|
def test_docker_auth_pool_keeps_account_identity_and_never_falls_back_when_empty(self):
|
|
source_config = {
|
|
'auth_pool': 'dockerhub_main',
|
|
'rate_limit_cooldown': 300,
|
|
}
|
|
secrets = {'auth_pools': {'dockerhub_main': [
|
|
{'name': 'account-a', 'username': 'user-a', 'token': 'secret-a'},
|
|
{'name': 'account-b', 'username': 'user-b', 'token': 'secret-b'},
|
|
]}}
|
|
state = {'sources': {'dockerhub': console_runner.default_source_state()}}
|
|
with mock.patch.object(console_runner, 'configure_docker_discovery_accounts') as configure, \
|
|
mock.patch.object(console_runner, 'configure_docker_discovery_tokens') as legacy, \
|
|
mock.patch('builtins.print'):
|
|
console_runner.configure_source_auth(
|
|
'dockerhub', source_config, state, secrets,
|
|
)
|
|
configure.assert_called_once_with(
|
|
[
|
|
{'name': 'account-a', 'username': 'user-a', 'token': 'secret-a'},
|
|
{'name': 'account-b', 'username': 'user-b', 'token': 'secret-b'},
|
|
],
|
|
cooldown_sec=300,
|
|
)
|
|
legacy.assert_not_called()
|
|
|
|
state['sources']['dockerhub']['auth_status']['account-a'] = {
|
|
'status': 'dead', 'disabled_until': 'manual', 'disabled_reason': 'auth_invalid',
|
|
}
|
|
state['sources']['dockerhub']['auth_status']['account-b'] = {
|
|
'status': 'dead', 'disabled_until': 'manual', 'disabled_reason': 'auth_invalid',
|
|
}
|
|
with mock.patch.object(console_runner, 'configure_docker_discovery_accounts') as configure, \
|
|
mock.patch.object(console_runner, 'configure_docker_discovery_tokens') as legacy, \
|
|
mock.patch('builtins.print'):
|
|
console_runner.configure_source_auth(
|
|
'dockerhub', source_config, state, secrets,
|
|
)
|
|
configure.assert_called_once_with([], cooldown_sec=300)
|
|
legacy.assert_not_called()
|
|
|
|
def test_docker_discovery_auth_does_not_require_scanner_runtime_or_create_config(self):
|
|
manager = scanner.DockerTokenManager()
|
|
accounts = [{'name': 'account-a', 'username': 'user-a', 'token': 'secret-a'}]
|
|
with mock.patch.object(scanner, 'docker_token_manager', manager), \
|
|
mock.patch.object(
|
|
scanner, 'require_scanner_runtime_initialized',
|
|
side_effect=RuntimeError('scanner runtime unavailable'),
|
|
) as require_runtime, \
|
|
mock.patch.object(scanner, 'create_docker_config_dir') as create_config:
|
|
scanner.configure_docker_discovery_accounts(accounts, cooldown_sec=300)
|
|
self.assertEqual(manager.account_count(), 1)
|
|
self.assertEqual(manager.accounts[0].config_dir, '')
|
|
create_config.assert_not_called()
|
|
require_runtime.assert_not_called()
|
|
|
|
with self.assertRaisesRegex(RuntimeError, 'scanner runtime unavailable'):
|
|
scanner.configure_docker_accounts(accounts, cooldown_sec=300)
|
|
require_runtime.assert_called_once_with()
|
|
|
|
def test_docker_auth_events_persist_only_fixed_token_free_status(self):
|
|
source_config = {'auth_pool': 'dockerhub_main', 'rate_limit_cooldown': 300}
|
|
secrets = {'auth_pools': {'dockerhub_main': [
|
|
{'name': 'account-a', 'username': 'user-a', 'token': 'sentinel-secret'},
|
|
]}}
|
|
state = {'sources': {'dockerhub': console_runner.default_source_state()}}
|
|
events = [{
|
|
'name': 'account-a', 'category': 'rate_limit',
|
|
'reset_at': '2026-08-29T12:00:00+00:00',
|
|
'message': 'Docker hub_tags HTTP 429',
|
|
}]
|
|
with mock.patch.object(console_runner, 'drain_docker_auth_events', return_value=events):
|
|
console_runner.persist_docker_auth_events(
|
|
'dockerhub', source_config, state, secrets,
|
|
)
|
|
status = state['sources']['dockerhub']['auth_status']['account-a']
|
|
self.assertEqual(status['disabled_reason'], 'rate_limit')
|
|
self.assertEqual(status['last_error'], 'Docker hub_tags HTTP 429')
|
|
self.assertNotIn('sentinel-secret', json.dumps(state))
|
|
|
|
def test_structured_digest_cache_roundtrip_preserves_target(self):
|
|
digest = 'sha256:' + ('5' * 64)
|
|
repointed_digest = 'sha256:' + ('7' * 64)
|
|
target = serialize_docker_tag_target(f'owner/repo:latest@{digest}', '2026-07-20T01:02:03Z')
|
|
same_image = serialize_docker_tag_target(f'owner/repo:latest@{digest}', '2026-07-21T01:02:03Z')
|
|
repointed = serialize_docker_tag_target(
|
|
f'owner/repo:latest@{repointed_digest}', '2026-07-21T01:02:03Z',
|
|
)
|
|
parsed = parse_docker_target(target)
|
|
self.assertEqual(parsed['image'], f'owner/repo:latest@{digest}')
|
|
self.assertEqual(parsed['revision'], '2026-07-20T01:02:03Z')
|
|
self.assertEqual(normalize_target(target, 'docker'), normalize_target(same_image, 'docker'))
|
|
self.assertNotEqual(normalize_target(target, 'docker'), normalize_target(repointed, 'docker'))
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
path = os.path.join(temp_dir, 'docker-cache.sqlite')
|
|
with mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_path', path), \
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_rows', 10), \
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_age_sec', 3600), \
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_bytes', 1024 * 1024), \
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_min_free_bytes', 0):
|
|
scanner._dockerhub_tag_cache_initialized.discard(path)
|
|
self.assertTrue(scanner.put_dockerhub_tag_cache(
|
|
'owner/repo', None, 1, 'ok', [target], 3600,
|
|
tag_records=[{'name': 'latest', 'target': target, 'updated_at': 1.0}],
|
|
))
|
|
self.assertEqual(scanner.get_dockerhub_tag_cache('owner/repo', None, 1), [target])
|
|
self.assertTrue(scanner.put_dockerhub_tag_cache(
|
|
'owner/repo', None, 1, 'ok', [repointed], 3600,
|
|
tag_records=[{'name': 'latest', 'target': repointed, 'updated_at': 2.0}],
|
|
))
|
|
self.assertEqual(scanner.get_dockerhub_tag_cache('owner/repo', None, 1), [repointed])
|
|
self.assertFalse(scanner.put_dockerhub_tag_cache(
|
|
'owner/mutable', None, 1, 'ok', ['owner/mutable:latest'], 3600,
|
|
))
|
|
self.assertIsNone(scanner.get_dockerhub_tag_cache('owner/mutable', None, 1))
|
|
|
|
def test_concurrent_cache_writes_are_serialized_and_reads_do_not_maintain(self):
|
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
path = os.path.join(temp_dir, 'docker-cache.sqlite')
|
|
options = (
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_path', path),
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_rows', 100),
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_age_sec', 3600),
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_max_bytes', 16 * 1024 * 1024),
|
|
mock.patch.object(scanner.scan_config, 'dockerhub_tag_cache_min_free_bytes', 0),
|
|
)
|
|
with options[0], options[1], options[2], options[3], options[4]:
|
|
scanner._dockerhub_tag_cache_initialized.discard(path)
|
|
digest = 'sha256:' + ('8' * 64)
|
|
first = f'owner/repo-0@{digest}'
|
|
self.assertTrue(scanner.put_dockerhub_tag_cache(
|
|
'owner/repo-0', None, 1, 'ok', [first], 3600,
|
|
tag_records=[{'name': 'latest', 'target': first, 'updated_at': 1.0}],
|
|
))
|
|
with mock.patch.object(scanner, 'maintain_dockerhub_tag_cache') as maintain:
|
|
self.assertEqual(scanner.get_dockerhub_tag_cache('owner/repo-0', None, 1), [first])
|
|
maintain.assert_not_called()
|
|
|
|
barrier = threading.Barrier(12)
|
|
outcomes = []
|
|
outcome_lock = threading.Lock()
|
|
|
|
def write(index):
|
|
target = f'owner/repo-{index}@{digest}'
|
|
barrier.wait()
|
|
result = scanner.put_dockerhub_tag_cache(
|
|
f'owner/repo-{index}', None, 1, 'ok', [target], 3600,
|
|
tag_records=[{'name': 'latest', 'target': target, 'updated_at': float(index)}],
|
|
)
|
|
with outcome_lock:
|
|
outcomes.append(result)
|
|
|
|
threads = [threading.Thread(target=write, args=(index,)) for index in range(1, 13)]
|
|
for thread in threads:
|
|
thread.start()
|
|
for thread in threads:
|
|
thread.join(10)
|
|
self.assertFalse(thread.is_alive())
|
|
self.assertEqual(outcomes, [True] * 12)
|
|
|
|
def test_scan_docker_image_uses_only_digest_pinned_image_argument(self):
|
|
digest = 'sha256:' + ('6' * 64)
|
|
image = f'registry.example.com:5000/ns/image:Latest@{digest}'
|
|
target = serialize_docker_tag_target(image, '2026-07-20T00:00:00Z')
|
|
completed = json.dumps({'level': 'info-0', 'msg': 'finished scanning'})
|
|
with mock.patch.object(scanner, 'get_trufflehog_cmd', return_value='trufflehog'), \
|
|
mock.patch.object(scanner, 'run_command_streamed', return_value=scanner.streamed_output_from_text('', completed, 0)) as run:
|
|
result = scanner.scan_docker_image(target, trufflehog_concurrency=4)
|
|
self.assertEqual(result['errors'], [])
|
|
command = run.call_args.args[0]
|
|
self.assertEqual(command[command.index('--image') + 1], image)
|
|
self.assertNotIn(target, command)
|
|
self.assertIn('--local-dev', command)
|
|
self.assertIn('--no-update', command)
|
|
self.assertEqual(command[command.index('--concurrency') + 1], '4')
|
|
|
|
with 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)) as run:
|
|
scanner.scan_huggingface_space('owner/space')
|
|
self.assertNotIn('--local-dev', run.call_args.args[0])
|
|
|
|
configured = console_runner.load_config(str(APP_DIR / 'config.yaml'))
|
|
args = console_runner.build_args_from_source_config(
|
|
'dockerhub', configured['sources']['dockerhub'], configured['global'], 'test',
|
|
)
|
|
with mock.patch('builtins.print'):
|
|
options = console_runner.prepare_scan_options(args, 1)
|
|
self.assertEqual(args.timeout, 600)
|
|
self.assertEqual(args.docker_images_per_repository, 3)
|
|
self.assertEqual(args.docker_repository_refresh_interval_sec, 86400)
|
|
self.assertEqual(args.docker_repository_refresh_max_per_cycle, 0)
|
|
self.assertEqual(options['trufflehog_concurrency'], 4)
|
|
|
|
with mock.patch.object(scanner, 'run_command_streamed') as rejected_run:
|
|
for mutable in (
|
|
'owner/repo',
|
|
'owner/repo:latest',
|
|
'{"image":"owner/repo:latest","revision":"2026-07-20T00:00:00Z","schema":"docker-tag-v1"}',
|
|
):
|
|
with self.subTest(mutable=mutable):
|
|
rejected = scanner.scan_docker_image(mutable)
|
|
self.assertEqual(rejected['error_class'], 'invalid_target')
|
|
self.assertFalse(rejected['retryable'])
|
|
rejected_run.assert_not_called()
|
|
|
|
def test_timestamp_cannot_substitute_for_digest_and_config_requires_digest(self):
|
|
with self.assertRaisesRegex(ValueError, 'sha256 digest'):
|
|
serialize_docker_tag_target('owner/repo:latest', '2026-07-20T01:02:03Z')
|
|
configured = console_runner.load_config(str(APP_DIR / 'config.yaml'))
|
|
self.assertIs(configured['sources']['dockerhub']['require_digest'], True)
|
|
|
|
|
|
if __name__ == '__main__':
|
|
unittest.main()
|