1462 lines
58 KiB
Python
1462 lines
58 KiB
Python
from collections.abc import Hashable
|
|
from dataclasses import dataclass
|
|
import hashlib
|
|
import ipaddress
|
|
import json
|
|
import math
|
|
import posixpath
|
|
import re
|
|
from urllib.parse import urlsplit
|
|
|
|
import yaml
|
|
|
|
from capacity_model import MAX_RESULT_BUNDLE_BYTES, validate_remote_assignment_capacity
|
|
from managed_files import (
|
|
ManagedFileConfigurationError,
|
|
managed_file_root_registry_from_config,
|
|
normalize_managed_file_roots,
|
|
)
|
|
from query_policy import QueryPolicyError, validate_rejected_query_policy
|
|
|
|
|
|
_MESSAGES = {
|
|
'invalid_input': 'YAML document input is invalid',
|
|
'size': 'YAML document exceeds its byte bound',
|
|
'encoding': 'YAML document is not valid UTF-8',
|
|
'duplicate_key': 'YAML document contains a duplicate mapping key',
|
|
'syntax': 'YAML document is invalid',
|
|
'mapping_root': 'Runtime document root must be a mapping',
|
|
'unknown_key': 'Runtime document contains an unsupported field',
|
|
'type': 'Runtime document field has an invalid type',
|
|
'bounds': 'Runtime document field exceeds its bound',
|
|
'schema': 'Runtime document is incomplete',
|
|
'core_profile': 'Runtime core profile is invalid',
|
|
'auth_pool': 'Runtime credential pool is invalid',
|
|
'reference': 'Runtime document reference is invalid',
|
|
'capability': 'Runtime package capability is invalid',
|
|
'deployment_path': 'Runtime deployment path is invalid',
|
|
}
|
|
_MERGE_KEY = object()
|
|
_MISSING = object()
|
|
|
|
MAX_DOCUMENT_DEPTH = 64
|
|
MAX_DOCUMENT_NODES = 100000
|
|
MAX_CONFIG_DOCUMENT_BYTES = 4 * 1024 * 1024
|
|
MAX_SECRETS_DOCUMENT_BYTES = 1024 * 1024
|
|
MAX_MAPPING_ENTRIES = 10000
|
|
MAX_SEQUENCE_ITEMS = 100000
|
|
MAX_MAPPING_KEY_BYTES = 512
|
|
MAX_SCALAR_BYTES = 1024 * 1024
|
|
MAX_PATH_BYTES = 4096
|
|
MAX_NAME_BYTES = 128
|
|
MAX_AUTH_POOLS = 64
|
|
MAX_AUTH_ENTRIES = 256
|
|
MAX_COMPATIBILITY_PROFILES = 16
|
|
MAX_PACKAGE_CAPABILITIES = 16
|
|
_MIN_INTEGER = -(1 << 63)
|
|
_MAX_INTEGER = (1 << 63) - 1
|
|
|
|
_SAFE_DOCUMENTS = frozenset({'config', 'secrets', 'package_capabilities', 'schema'})
|
|
_SAFE_FIELD_PATH = re.compile(r'^[a-z0-9_.\[\]-]{1,512}$')
|
|
_ENV_NAME = re.compile(r'^[A-Z_][A-Z0-9_]{0,127}$')
|
|
_PLACEHOLDER = re.compile(r'{([A-Za-z_][A-Za-z0-9_]*)}')
|
|
_CORE_SOURCES = ('gitlab', 'dockerhub', 'huggingface')
|
|
_KNOWN_CAPABILITIES = frozenset({
|
|
('github', 'github', 'exact_git_v1'),
|
|
('gitlab', 'gitlab', 'exact_git_v1'),
|
|
('dockerhub', 'docker', 'docker_direct_v1'),
|
|
('huggingface', 'huggingface', 'huggingface_space_v1'),
|
|
})
|
|
_SOURCE_CAPABILITIES = {
|
|
'gitlab': ('gitlab', 'gitlab', 'exact_git_v1'),
|
|
'dockerhub': ('dockerhub', 'docker', 'docker_direct_v1'),
|
|
'huggingface': ('huggingface', 'huggingface', 'huggingface_space_v1'),
|
|
}
|
|
_REQUIRED_CONFIG_PATHS = (
|
|
('global',),
|
|
('supervisor',),
|
|
('sources',),
|
|
('global', 'root_dir'),
|
|
('global', 'project_dir'),
|
|
('global', 'runtime_dir'),
|
|
('global', 'postgres_data_dir'),
|
|
('global', 'postgres_bin_dir'),
|
|
('global', 'result_bundle_dir'),
|
|
('global', 'work_dir'),
|
|
('global', 'control_dir'),
|
|
('global', 'secrets_file'),
|
|
('supervisor', 'enabled_sources'),
|
|
('supervisor', 'control_dir'),
|
|
('supervisor', 'worker_api'),
|
|
('supervisor', 'worker_api', 'enabled'),
|
|
('supervisor', 'worker_api', 'sources'),
|
|
('supervisor', 'worker_api', 'auth_entries'),
|
|
('supervisor', 'worker_api', 'compatibility_profiles'),
|
|
('sources', 'gitlab'),
|
|
('sources', 'gitlab', 'enabled'),
|
|
('sources', 'gitlab', 'auth_pool'),
|
|
('sources', 'dockerhub'),
|
|
('sources', 'dockerhub', 'enabled'),
|
|
('sources', 'dockerhub', 'auth_pool'),
|
|
('sources', 'huggingface'),
|
|
('sources', 'huggingface', 'enabled'),
|
|
('sources', 'huggingface', 'auth_pool'),
|
|
)
|
|
_FIXED_PATHS = {
|
|
'root_dir': '/opt/truf',
|
|
'project_dir': '/opt/truf/app',
|
|
'runtime_dir': '/data/runtime-linux',
|
|
'postgres_data_dir': '/data/postgres-linux',
|
|
'postgres_bin_dir': '/usr/lib/postgresql/16/bin',
|
|
'result_bundle_dir': '/data/scanner-result-bundles',
|
|
'work_dir': '/data/scanner-work',
|
|
'control_dir': '/run/truf/control',
|
|
'secrets_file': '/data/config/secrets.yaml',
|
|
}
|
|
_GLOBAL_PATH_FIELDS = frozenset({
|
|
'result_bundle_dir', 'work_dir', 'root_dir', 'project_dir', 'runtime_dir',
|
|
'postgres_data_dir', 'postgres_bin_dir', 'control_dir', 'secrets_file',
|
|
'result_spool_dir', 'legacy_result_spool_dir', 'results_dir', 'queue_dir',
|
|
'state_dir', 'log_dir', 'keycheck_dir', 'postman_cache_dir',
|
|
'gharchive_cache_dir', 'database_path', 'state_file', 'api_proxy_file',
|
|
'download_proxy_file', 'dashboard_db_path', 'scan_limiter_db',
|
|
'dockerhub_tag_cache_path', 'proxy_file', 'trufflehog_path',
|
|
'trufflehog_config',
|
|
})
|
|
_SUPERVISOR_PATH_FIELDS = frozenset({
|
|
'log_dir', 'control_dir', 'instance_file', 'lock_file', 'supervisor_log',
|
|
'status_file', 'dashboard_log', 'state_dir',
|
|
})
|
|
_KEYCHECK_PATH_FIELDS = frozenset({
|
|
'input', 'proxy_file', 'keycheck_dir', 'summary_tsv', 'summary_json',
|
|
'alive_summary_tsv',
|
|
})
|
|
_SOURCE_PATH_FIELDS = frozenset({
|
|
'target_file', 'trufflehog_config', 'postman_cache_dir',
|
|
'gharchive_cache_dir',
|
|
})
|
|
_CONFIG_TEMPLATE_SCHEMA_SHA256 = (
|
|
'45d957d8168c796a9d4081ab0936d83d797c4675c8d477baf63195117363a9d8'
|
|
)
|
|
|
|
|
|
class RuntimeDocumentError(ValueError):
|
|
def __init__(self, category, line=None, column=None, *, document=None, path=None):
|
|
self.category = str(category)
|
|
self.line = int(line) if line is not None else None
|
|
self.column = int(column) if column is not None else None
|
|
self.document = document if document in _SAFE_DOCUMENTS else None
|
|
self.path = (
|
|
path if type(path) is str and _SAFE_FIELD_PATH.fullmatch(path) else None
|
|
)
|
|
message = _MESSAGES[self.category]
|
|
if self.line is not None and self.column is not None:
|
|
message += f' at line {self.line}, column {self.column}'
|
|
if self.document is not None:
|
|
message += f' in {self.document}'
|
|
if self.path is not None:
|
|
message += f' at {self.path}'
|
|
super().__init__(message)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ValidatedRuntimeDocuments:
|
|
config: dict
|
|
secrets: dict
|
|
|
|
|
|
class _MarkedDocumentError(Exception):
|
|
def __init__(self, mark):
|
|
self.mark = mark
|
|
|
|
|
|
class _DuplicateKeyError(_MarkedDocumentError):
|
|
pass
|
|
|
|
|
|
class _DocumentSyntaxError(_MarkedDocumentError):
|
|
pass
|
|
|
|
|
|
class _StrictSafeLoader(yaml.SafeLoader):
|
|
def _mapping_key(self, key_node, deep):
|
|
key = None
|
|
try:
|
|
if key_node.tag == 'tag:yaml.org,2002:merge':
|
|
return _MERGE_KEY
|
|
key = self.construct_object(key_node, deep=deep)
|
|
if not isinstance(key, Hashable):
|
|
raise _DocumentSyntaxError(key_node.start_mark)
|
|
return key
|
|
finally:
|
|
key_node = deep = key = None
|
|
|
|
def _reject_duplicate_keys(self, pairs, deep):
|
|
seen = key_node = _value_node = key = duplicate = None
|
|
try:
|
|
seen = {}
|
|
for key_node, _value_node in pairs:
|
|
key = self._mapping_key(key_node, deep)
|
|
try:
|
|
duplicate = key in seen
|
|
except Exception:
|
|
raise _DocumentSyntaxError(key_node.start_mark) from None
|
|
if duplicate:
|
|
raise _DuplicateKeyError(key_node.start_mark)
|
|
seen[key] = None
|
|
finally:
|
|
pairs = deep = seen = key_node = _value_node = key = duplicate = None
|
|
|
|
def flatten_mapping(self, node):
|
|
try:
|
|
# SafeLoader recursively calls this method for anonymous merge sources.
|
|
self._reject_duplicate_keys(node.value, False)
|
|
return super().flatten_mapping(node)
|
|
finally:
|
|
node = None
|
|
|
|
def construct_mapping(self, node, deep=False):
|
|
mapping = key_node = value_node = key = None
|
|
try:
|
|
if not isinstance(node, yaml.MappingNode):
|
|
raise _DocumentSyntaxError(node.start_mark)
|
|
|
|
self.flatten_mapping(node)
|
|
self._reject_duplicate_keys(node.value, deep)
|
|
|
|
mapping = {}
|
|
for key_node, value_node in node.value:
|
|
key = self._mapping_key(key_node, deep)
|
|
mapping[key] = self.construct_object(value_node, deep=deep)
|
|
return mapping
|
|
finally:
|
|
node = deep = mapping = key_node = value_node = key = None
|
|
|
|
|
|
def _location(mark):
|
|
if mark is None:
|
|
return None, None
|
|
line = getattr(mark, 'line', None)
|
|
column = getattr(mark, 'column', None)
|
|
if type(line) is not int or type(column) is not int:
|
|
return None, None
|
|
return line + 1, column + 1
|
|
|
|
|
|
def _parse_yaml_document(payload):
|
|
text = None
|
|
try:
|
|
text = payload.decode('utf-8', errors='strict')
|
|
except UnicodeDecodeError:
|
|
pass
|
|
if text is None:
|
|
return None, ('encoding', None, None)
|
|
|
|
value = None
|
|
failure = None
|
|
loader = None
|
|
try:
|
|
loader = _StrictSafeLoader(text)
|
|
value = loader.get_single_data()
|
|
except _DuplicateKeyError as exc:
|
|
failure = ('duplicate_key', *_location(exc.mark))
|
|
except _DocumentSyntaxError as exc:
|
|
failure = ('syntax', *_location(exc.mark))
|
|
except Exception as exc:
|
|
mark = getattr(exc, 'problem_mark', None) or getattr(exc, 'context_mark', None)
|
|
failure = ('syntax', *_location(mark))
|
|
except BaseException:
|
|
if loader is not None:
|
|
try:
|
|
loader.dispose()
|
|
except Exception:
|
|
pass
|
|
loader = None
|
|
value = None
|
|
text = None
|
|
payload = None
|
|
raise
|
|
finally:
|
|
if loader is not None:
|
|
try:
|
|
loader.dispose()
|
|
except Exception:
|
|
failure = failure or ('syntax', None, None)
|
|
|
|
if failure is not None:
|
|
value = None
|
|
return value, failure
|
|
|
|
|
|
def load_yaml_document(payload, *, max_bytes):
|
|
if type(payload) is not bytes or type(max_bytes) is not int or max_bytes <= 0:
|
|
payload = None
|
|
max_bytes = None
|
|
raise RuntimeDocumentError('invalid_input')
|
|
if len(payload) > max_bytes:
|
|
payload = None
|
|
raise RuntimeDocumentError('size')
|
|
|
|
value = failure = None
|
|
try:
|
|
value, failure = _parse_yaml_document(payload)
|
|
finally:
|
|
payload = None
|
|
max_bytes = None
|
|
|
|
if failure is not None:
|
|
raise RuntimeDocumentError(*failure)
|
|
return value
|
|
|
|
|
|
def _preview_document(payload, max_bytes, document):
|
|
try:
|
|
value = load_yaml_document(payload, max_bytes=max_bytes)
|
|
except RuntimeDocumentError as exc:
|
|
return None, (
|
|
exc.category, exc.line, exc.column, document, exc.path,
|
|
)
|
|
finally:
|
|
payload = None
|
|
return value, None
|
|
|
|
|
|
def _field_path(path):
|
|
return '.'.join(path) if path else 'root'
|
|
|
|
|
|
def _failure(category, document, path):
|
|
return category, document, _field_path(path)
|
|
|
|
|
|
def _consume(state, count=1):
|
|
state['nodes'] += count
|
|
return state['nodes'] <= MAX_DOCUMENT_NODES
|
|
|
|
|
|
def _bounded_text(value, limit=MAX_SCALAR_BYTES, *, nonempty=False):
|
|
try:
|
|
if type(value) is not str or '\x00' in value:
|
|
return False
|
|
try:
|
|
size = len(value.encode('utf-8', errors='strict'))
|
|
except UnicodeError:
|
|
return False
|
|
return size <= limit and (not nonempty or bool(value))
|
|
finally:
|
|
value = None
|
|
|
|
|
|
def _bounded_name(value):
|
|
try:
|
|
return (
|
|
_bounded_text(value, MAX_NAME_BYTES, nonempty=True)
|
|
and value == value.strip()
|
|
)
|
|
finally:
|
|
value = None
|
|
|
|
|
|
def _normalize_string_list(value, path, state, *, max_items=MAX_SEQUENCE_ITEMS):
|
|
normalized = item = None
|
|
try:
|
|
if type(value) is not list:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > max_items:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = []
|
|
for item in value:
|
|
if not _consume(state) or not _bounded_text(item, nonempty=True):
|
|
return None, _failure('bounds', 'config', path + ('item',))
|
|
normalized.append(item)
|
|
return normalized, None
|
|
finally:
|
|
value = path = state = normalized = item = None
|
|
|
|
|
|
def _normalize_dynamic_config(value, path, state):
|
|
normalized = key = entry_name = profile_name = raw_profile = None
|
|
item_path = package_manifest = profile = sources = failure = item = None
|
|
query = raw_override = override = name = maximum = None
|
|
try:
|
|
if path == ('supervisor', 'worker_api', 'admin', 'managed_file_roots'):
|
|
try:
|
|
normalized, _registry = normalize_managed_file_roots(value)
|
|
except ManagedFileConfigurationError as exc:
|
|
return None, _failure(exc.category, 'config', path + exc.field)
|
|
return normalized, None
|
|
|
|
if path in (
|
|
('supervisor', 'worker_api', 'sources'),
|
|
('supervisor', 'defaults', 'extra_args'),
|
|
):
|
|
return _normalize_string_list(value, path, state)
|
|
|
|
if path == ('supervisor', 'worker_api', 'assignment_ttl_seconds_by_source'):
|
|
if type(value) is not dict:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > len(_CORE_SOURCES):
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for key, item in value.items():
|
|
item_path = path + (key,) if type(key) is str else path + ('field',)
|
|
if not _consume(state):
|
|
return None, _failure('bounds', 'config', item_path)
|
|
if key not in _CORE_SOURCES:
|
|
return None, _failure('unknown_key', 'config', item_path)
|
|
if type(item) is not int or not 60 <= item <= 7 * 24 * 60 * 60:
|
|
return None, _failure('bounds', 'config', item_path)
|
|
normalized[key] = item
|
|
return normalized, None
|
|
|
|
if path == ('supervisor', 'worker_api', 'auth_entries'):
|
|
if type(value) is not dict:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > len(_CORE_SOURCES) + 1:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for key, entry_name in value.items():
|
|
if not _consume(state) or not _bounded_name(key) or not _bounded_name(entry_name):
|
|
return None, _failure('bounds', 'config', path + ('item',))
|
|
normalized[key] = entry_name
|
|
return normalized, None
|
|
|
|
if path == ('supervisor', 'worker_api', 'compatibility_profiles'):
|
|
if type(value) is not dict:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > MAX_COMPATIBILITY_PROFILES:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for profile_name, raw_profile in value.items():
|
|
item_path = path + ('profile',)
|
|
if not _consume(state) or not _bounded_name(profile_name):
|
|
return None, _failure('bounds', 'config', item_path)
|
|
if type(raw_profile) is not dict:
|
|
return None, _failure('type', 'config', item_path)
|
|
if set(raw_profile) - {'package_manifest', 'sources'}:
|
|
return None, _failure('unknown_key', 'config', item_path)
|
|
package_manifest = raw_profile.get('package_manifest', _MISSING)
|
|
if package_manifest is _MISSING:
|
|
return None, _failure('schema', 'config', item_path + ('package_manifest',))
|
|
if not _consume(state) or not _bounded_text(package_manifest, MAX_PATH_BYTES, nonempty=True):
|
|
return None, _failure('bounds', 'config', item_path + ('package_manifest',))
|
|
profile = {'package_manifest': package_manifest}
|
|
if 'sources' in raw_profile:
|
|
sources, failure = _normalize_string_list(
|
|
raw_profile['sources'], item_path + ('sources',), state,
|
|
max_items=len(_CORE_SOURCES),
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
profile['sources'] = sources
|
|
normalized[profile_name] = profile
|
|
return normalized, None
|
|
|
|
if (
|
|
path == ('keychecks', 'env')
|
|
or len(path) == 4
|
|
and path[0] == 'supervisor'
|
|
and path[1] == 'sources'
|
|
and path[3] == 'env'
|
|
):
|
|
if type(value) is not dict:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > 128:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for key, item in value.items():
|
|
if (
|
|
not _consume(state)
|
|
or type(key) is not str
|
|
or _ENV_NAME.fullmatch(key) is None
|
|
or not _bounded_text(item)
|
|
):
|
|
return None, _failure('bounds', 'config', path + ('item',))
|
|
normalized[key] = item
|
|
return normalized, None
|
|
|
|
if (
|
|
len(path) == 3
|
|
and path[0] == 'sources'
|
|
and path[2] == 'query_overrides'
|
|
):
|
|
if type(value) is not dict:
|
|
return None, _failure('type', 'config', path)
|
|
if len(value) > MAX_MAPPING_ENTRIES:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for query, raw_override in value.items():
|
|
item_path = path + ('item',)
|
|
if not _consume(state) or not _bounded_text(query, 512, nonempty=True):
|
|
return None, _failure('bounds', 'config', item_path)
|
|
if type(raw_override) is not dict:
|
|
return None, _failure('type', 'config', item_path)
|
|
if set(raw_override) - {'pages', 'per_page', 'max_targets'}:
|
|
return None, _failure('unknown_key', 'config', item_path)
|
|
override = {}
|
|
for name, item in raw_override.items():
|
|
maximum = 100000 if name == 'max_targets' else 1000
|
|
if (
|
|
not _consume(state)
|
|
or type(item) is not int
|
|
or not 1 <= item <= maximum
|
|
):
|
|
return None, _failure('bounds', 'config', item_path + (name,))
|
|
override[name] = item
|
|
normalized[query] = override
|
|
return normalized, None
|
|
|
|
return _MISSING
|
|
finally:
|
|
value = path = state = normalized = key = entry_name = None
|
|
profile_name = raw_profile = item_path = package_manifest = profile = None
|
|
sources = failure = item = query = raw_override = override = None
|
|
name = maximum = None
|
|
|
|
|
|
def _normalize_scalar(value, template, path):
|
|
try:
|
|
if type(value) is not type(template):
|
|
return None, _failure('type', 'config', path)
|
|
if type(value) is int and not 0 <= value <= _MAX_INTEGER:
|
|
return None, _failure('bounds', 'config', path)
|
|
if type(value) is float and (not math.isfinite(value) or value < 0):
|
|
return None, _failure('bounds', 'config', path)
|
|
if type(value) is str and not _bounded_text(value):
|
|
return None, _failure('bounds', 'config', path)
|
|
if type(value) not in {type(None), bool, int, float, str}:
|
|
return None, _failure('type', 'config', path)
|
|
return value, None
|
|
finally:
|
|
value = template = path = None
|
|
|
|
|
|
def _normalize_config_shape(value, template, path, state, depth=0):
|
|
identity = dynamic = normalized = key = item = copied = failure = None
|
|
try:
|
|
if depth > MAX_DOCUMENT_DEPTH or not _consume(state):
|
|
return None, _failure('bounds', 'config', path)
|
|
|
|
if type(value) in (dict, list):
|
|
identity = id(value)
|
|
if identity in state['active']:
|
|
return None, _failure('bounds', 'config', path)
|
|
state['active'].add(identity)
|
|
try:
|
|
dynamic = _normalize_dynamic_config(value, path, state)
|
|
if dynamic is not _MISSING:
|
|
return dynamic
|
|
|
|
if type(template) is not type(value):
|
|
return None, _failure('type', 'config', path)
|
|
if type(value) is dict:
|
|
if len(value) > MAX_MAPPING_ENTRIES:
|
|
return None, _failure('bounds', 'config', path)
|
|
normalized = {}
|
|
for key, item in value.items():
|
|
if (
|
|
type(key) is not str
|
|
or not _bounded_text(key, MAX_MAPPING_KEY_BYTES, nonempty=True)
|
|
):
|
|
return None, _failure('type', 'config', path + ('field',))
|
|
if key not in template:
|
|
if (
|
|
len(path) == 2
|
|
and path[0] == 'sources'
|
|
and key in _SOURCE_PATH_FIELDS
|
|
):
|
|
copied, failure = _normalize_scalar(
|
|
item, '', path + (key,),
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized[key] = copied
|
|
continue
|
|
if (
|
|
len(path) == 3
|
|
and path[:2] == ('supervisor', 'sources')
|
|
and key == 'enabled'
|
|
):
|
|
copied, failure = _normalize_scalar(
|
|
item, False, path + (key,),
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized[key] = copied
|
|
continue
|
|
if (
|
|
len(path) == 3
|
|
and path[:2] == ('supervisor', 'sources')
|
|
and key == 'env'
|
|
):
|
|
copied, failure = _normalize_dynamic_config(
|
|
item, path + (key,), state,
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized[key] = copied
|
|
continue
|
|
return None, _failure('unknown_key', 'config', path + ('unknown',))
|
|
copied, failure = _normalize_config_shape(
|
|
item, template[key], path + (key,), state, depth + 1,
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized[key] = copied
|
|
return normalized, None
|
|
|
|
if len(value) > MAX_SEQUENCE_ITEMS:
|
|
return None, _failure('bounds', 'config', path)
|
|
if not template:
|
|
if value:
|
|
return None, _failure('type', 'config', path)
|
|
return [], None
|
|
normalized = []
|
|
for item in value:
|
|
copied, failure = _normalize_config_shape(
|
|
item, template[0], path + ('item',), state, depth + 1,
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized.append(copied)
|
|
return normalized, None
|
|
finally:
|
|
state['active'].remove(identity)
|
|
|
|
return _normalize_scalar(value, template, path)
|
|
finally:
|
|
value = template = path = state = identity = dynamic = None
|
|
normalized = key = item = copied = failure = None
|
|
|
|
|
|
def _dynamic_schema_kind(path):
|
|
if path == ('supervisor', 'worker_api', 'admin', 'managed_file_roots'):
|
|
return 'managed_file_roots'
|
|
if path in (
|
|
('supervisor', 'worker_api', 'sources'),
|
|
('supervisor', 'defaults', 'extra_args'),
|
|
):
|
|
return 'string_list'
|
|
if path == ('supervisor', 'worker_api', 'assignment_ttl_seconds_by_source'):
|
|
return 'assignment_ttl_seconds_by_source'
|
|
if path == ('supervisor', 'worker_api', 'auth_entries'):
|
|
return 'auth_entries'
|
|
if path == ('supervisor', 'worker_api', 'compatibility_profiles'):
|
|
return 'compatibility_profiles'
|
|
if (
|
|
path == ('keychecks', 'env')
|
|
or len(path) == 4
|
|
and path[0] == 'supervisor'
|
|
and path[1] == 'sources'
|
|
and path[3] == 'env'
|
|
):
|
|
return 'environment'
|
|
if (
|
|
len(path) == 3
|
|
and path[0] == 'sources'
|
|
and path[2] == 'query_overrides'
|
|
):
|
|
return 'query_overrides'
|
|
return None
|
|
|
|
|
|
def _config_template_schema_identity(value, path=()):
|
|
dynamic_kind = scalar_names = None
|
|
try:
|
|
dynamic_kind = _dynamic_schema_kind(path)
|
|
if dynamic_kind is not None:
|
|
return {'dynamic': dynamic_kind}
|
|
if type(value) is dict:
|
|
return {
|
|
'mapping': [
|
|
[key, _config_template_schema_identity(value[key], path + (key,))]
|
|
for key in sorted(value)
|
|
],
|
|
}
|
|
if type(value) is list:
|
|
return {
|
|
'sequence': (
|
|
_config_template_schema_identity(value[0], path + ('item',))
|
|
if value else None
|
|
),
|
|
}
|
|
scalar_names = {
|
|
type(None): 'null', bool: 'bool', int: 'int', float: 'float', str: 'str',
|
|
}
|
|
return {'scalar': scalar_names.get(type(value), 'unsupported')}
|
|
finally:
|
|
value = path = dynamic_kind = scalar_names = None
|
|
|
|
|
|
def _config_template_schema_hash(value):
|
|
encoded = None
|
|
try:
|
|
encoded = json.dumps(
|
|
_config_template_schema_identity(value),
|
|
ensure_ascii=True,
|
|
separators=(',', ':'),
|
|
sort_keys=True,
|
|
).encode('ascii')
|
|
return hashlib.sha256(encoded).hexdigest()
|
|
finally:
|
|
value = encoded = None
|
|
|
|
|
|
def _normalize_secrets(value):
|
|
state = {'nodes': 0, 'active': set()}
|
|
pools = normalized = active = None
|
|
pool_name = raw_entries = raw_entry = entries = names = None
|
|
name = token = entry = username = None
|
|
try:
|
|
if type(value) is not dict:
|
|
return None, _failure('mapping_root', 'secrets', ())
|
|
if set(value) - {'auth_pools'}:
|
|
return None, _failure('unknown_key', 'secrets', ('unknown',))
|
|
pools = value.get('auth_pools', {})
|
|
if type(pools) is not dict:
|
|
return None, _failure('type', 'secrets', ('auth_pools',))
|
|
if len(pools) > MAX_AUTH_POOLS:
|
|
return None, _failure('bounds', 'secrets', ('auth_pools',))
|
|
|
|
normalized = {}
|
|
active = {id(value), id(pools)}
|
|
state['active'].update(active)
|
|
if not _consume(state, 2):
|
|
return None, _failure('bounds', 'secrets', ())
|
|
for pool_name, raw_entries in pools.items():
|
|
pool_path = ('auth_pools', 'pool')
|
|
if not _bounded_name(pool_name) or type(raw_entries) is not list:
|
|
return None, _failure('auth_pool', 'secrets', pool_path)
|
|
if id(raw_entries) in state['active'] or len(raw_entries) > MAX_AUTH_ENTRIES:
|
|
return None, _failure('bounds', 'secrets', pool_path)
|
|
state['active'].add(id(raw_entries))
|
|
raw_entries_id = id(raw_entries)
|
|
entries = []
|
|
names = set()
|
|
try:
|
|
for raw_entry in raw_entries:
|
|
entry_path = pool_path + ('entry',)
|
|
if not _consume(state) or type(raw_entry) is not dict:
|
|
return None, _failure('auth_pool', 'secrets', entry_path)
|
|
if id(raw_entry) in state['active']:
|
|
return None, _failure('bounds', 'secrets', entry_path)
|
|
state['active'].add(id(raw_entry))
|
|
try:
|
|
if set(raw_entry) not in ({'name', 'token'}, {'name', 'username', 'token'}):
|
|
return None, _failure('auth_pool', 'secrets', entry_path)
|
|
name = raw_entry.get('name')
|
|
token = raw_entry.get('token')
|
|
if not _bounded_name(name) or not _bounded_text(token, nonempty=True):
|
|
return None, _failure('auth_pool', 'secrets', entry_path)
|
|
if name in names:
|
|
return None, _failure('auth_pool', 'secrets', entry_path + ('name',))
|
|
names.add(name)
|
|
entry = {'name': name}
|
|
if 'username' in raw_entry:
|
|
username = raw_entry['username']
|
|
if not _bounded_text(username, MAX_NAME_BYTES, nonempty=True):
|
|
return None, _failure('auth_pool', 'secrets', entry_path + ('username',))
|
|
entry['username'] = username
|
|
entry['token'] = token
|
|
entries.append(entry)
|
|
finally:
|
|
state['active'].remove(id(raw_entry))
|
|
finally:
|
|
state['active'].remove(raw_entries_id)
|
|
normalized[pool_name] = entries
|
|
return {'auth_pools': normalized}, None
|
|
finally:
|
|
value = pools = normalized = active = None
|
|
pool_name = raw_entries = raw_entry = entries = names = None
|
|
name = token = entry = username = None
|
|
state = None
|
|
|
|
|
|
def _get_path(value, path):
|
|
current = name = None
|
|
try:
|
|
current = value
|
|
for name in path:
|
|
if type(current) is not dict or name not in current:
|
|
return _MISSING
|
|
current = current[name]
|
|
return current
|
|
finally:
|
|
value = path = current = name = None
|
|
|
|
|
|
def _validate_required_fields(config):
|
|
path = None
|
|
try:
|
|
for path in _REQUIRED_CONFIG_PATHS:
|
|
if _get_path(config, path) is _MISSING:
|
|
return _failure('schema', 'config', path)
|
|
return None
|
|
finally:
|
|
config = path = None
|
|
|
|
|
|
def _validate_auth_references(config, secrets):
|
|
pools = sources = worker_config = raw_worker_sources = None
|
|
worker_sources = auth_entries = source_config = matches = None
|
|
docker_pool = source_name = pool_name = entry_name = None
|
|
try:
|
|
pools = secrets['auth_pools']
|
|
sources = config['sources']
|
|
docker_pool = sources['dockerhub'].get('auth_pool') or ''
|
|
|
|
for source_name, source_config in sources.items():
|
|
pool_name = source_config.get('auth_pool')
|
|
if pool_name and pool_name not in pools:
|
|
return _failure('reference', 'config', ('sources', source_name, 'auth_pool'))
|
|
|
|
if docker_pool:
|
|
if any('username' not in entry for entry in pools[docker_pool]):
|
|
return _failure('auth_pool', 'secrets', ('auth_pools', 'pool', 'entry'))
|
|
|
|
worker_config = config['supervisor']['worker_api']
|
|
raw_worker_sources = worker_config['sources']
|
|
if len(raw_worker_sources) != len(set(raw_worker_sources)):
|
|
return _failure('core_profile', 'config', ('supervisor', 'worker_api', 'sources'))
|
|
if raw_worker_sources:
|
|
if any(source not in _CORE_SOURCES for source in raw_worker_sources):
|
|
return _failure('core_profile', 'config', ('supervisor', 'worker_api', 'sources'))
|
|
worker_sources = tuple(source for source in _CORE_SOURCES if source in raw_worker_sources)
|
|
else:
|
|
worker_sources = _CORE_SOURCES
|
|
worker_config['sources'] = list(worker_sources)
|
|
|
|
auth_entries = worker_config['auth_entries']
|
|
if set(auth_entries) - (set(worker_sources) | {'github'}):
|
|
return _failure('reference', 'config', ('supervisor', 'worker_api', 'auth_entries'))
|
|
if set(auth_entries) - {'github', 'gitlab'}:
|
|
return _failure('reference', 'config', ('supervisor', 'worker_api', 'auth_entries'))
|
|
for source_name, entry_name in auth_entries.items():
|
|
source_config = sources.get(source_name)
|
|
if type(source_config) is not dict or source_config.get('enabled') is not True:
|
|
return _failure('reference', 'config', ('supervisor', 'worker_api', 'auth_entries'))
|
|
pool_name = source_config.get('auth_pool')
|
|
if not pool_name or pool_name not in pools:
|
|
return _failure('reference', 'config', ('supervisor', 'worker_api', 'auth_entries'))
|
|
matches = [entry for entry in pools[pool_name] if entry['name'] == entry_name]
|
|
if len(matches) != 1:
|
|
return _failure('reference', 'config', ('supervisor', 'worker_api', 'auth_entries'))
|
|
return None
|
|
finally:
|
|
config = secrets = pools = sources = None
|
|
worker_config = raw_worker_sources = worker_sources = None
|
|
auth_entries = source_config = matches = None
|
|
docker_pool = source_name = pool_name = entry_name = None
|
|
|
|
|
|
def _normalize_capabilities(package_capabilities, profiles, worker_sources, enabled):
|
|
if package_capabilities is None:
|
|
package_capabilities = {}
|
|
if type(package_capabilities) is not dict:
|
|
return None, _failure('capability', 'package_capabilities', ())
|
|
if set(package_capabilities) != set(profiles):
|
|
return None, _failure('capability', 'package_capabilities', ('profiles',))
|
|
if enabled and not profiles:
|
|
return None, _failure(
|
|
'capability', 'config',
|
|
('supervisor', 'worker_api', 'compatibility_profiles'),
|
|
)
|
|
|
|
normalized_capabilities = {}
|
|
for profile_name, evidence in package_capabilities.items():
|
|
path = ('profiles', 'profile')
|
|
if (
|
|
not _bounded_name(profile_name)
|
|
or type(evidence) is not dict
|
|
or set(evidence) != {'package_manifest', 'capabilities'}
|
|
or evidence.get('package_manifest') != profiles[profile_name]['package_manifest']
|
|
):
|
|
return None, _failure('capability', 'package_capabilities', path)
|
|
raw_capabilities = evidence['capabilities']
|
|
if type(raw_capabilities) is not list:
|
|
return None, _failure('capability', 'package_capabilities', path)
|
|
if not 1 <= len(raw_capabilities) <= MAX_PACKAGE_CAPABILITIES:
|
|
return None, _failure('capability', 'package_capabilities', path)
|
|
seen = set()
|
|
capabilities = []
|
|
for raw_item in raw_capabilities:
|
|
item_path = path + ('capability',)
|
|
if type(raw_item) is not dict or set(raw_item) != {
|
|
'source', 'platform', 'planning_kind',
|
|
}:
|
|
return None, _failure('capability', 'package_capabilities', item_path)
|
|
if any(type(raw_item[name]) is not str for name in raw_item):
|
|
return None, _failure('capability', 'package_capabilities', item_path)
|
|
capability = (
|
|
raw_item['source'], raw_item['platform'], raw_item['planning_kind'],
|
|
)
|
|
if capability not in _KNOWN_CAPABILITIES or capability in seen:
|
|
return None, _failure('capability', 'package_capabilities', item_path)
|
|
seen.add(capability)
|
|
capabilities.append(capability)
|
|
normalized_capabilities[profile_name] = frozenset(capabilities)
|
|
|
|
covered = set()
|
|
for profile_name, profile in profiles.items():
|
|
capabilities = normalized_capabilities[profile_name]
|
|
raw_sources = profile.get('sources') or [item[0] for item in capabilities]
|
|
if len(raw_sources) != len(set(raw_sources)):
|
|
return None, _failure(
|
|
'capability', 'config',
|
|
('supervisor', 'worker_api', 'compatibility_profiles', 'profile', 'sources'),
|
|
)
|
|
profile_sources = tuple(source for source in _CORE_SOURCES if source in raw_sources)
|
|
if (
|
|
not profile_sources
|
|
or set(raw_sources) != set(profile_sources)
|
|
or not set(profile_sources) <= set(worker_sources)
|
|
or any(_SOURCE_CAPABILITIES[source] not in capabilities for source in profile_sources)
|
|
):
|
|
return None, _failure(
|
|
'capability', 'config',
|
|
('supervisor', 'worker_api', 'compatibility_profiles', 'profile', 'sources'),
|
|
)
|
|
profile['sources'] = list(profile_sources)
|
|
covered.update(profile_sources)
|
|
if enabled and covered != set(worker_sources):
|
|
return None, _failure(
|
|
'capability', 'config',
|
|
('supervisor', 'worker_api', 'compatibility_profiles'),
|
|
)
|
|
return normalized_capabilities, None
|
|
|
|
|
|
def _validate_worker_config(config, package_capabilities):
|
|
worker_config = enabled = address_text = address = integer_bounds = None
|
|
name = minimum = maximum = value = source = effective_ttl = minimum_ttl = None
|
|
ttl_overrides = None
|
|
admin_config = default = origin = marker = parsed = parsed_port = None
|
|
_normalized = failure = managed_file_roots = None
|
|
try:
|
|
worker_config = config['supervisor']['worker_api']
|
|
enabled = worker_config['enabled']
|
|
if type(enabled) is not bool:
|
|
return _failure('type', 'config', ('supervisor', 'worker_api', 'enabled'))
|
|
|
|
address_text = worker_config.get('address', '127.0.0.1')
|
|
try:
|
|
address = ipaddress.ip_address(address_text)
|
|
except (TypeError, ValueError):
|
|
return _failure('bounds', 'config', ('supervisor', 'worker_api', 'address'))
|
|
if address.is_unspecified or address.is_multicast or not (
|
|
address.is_loopback or address.is_private
|
|
):
|
|
return _failure('bounds', 'config', ('supervisor', 'worker_api', 'address'))
|
|
|
|
integer_bounds = {
|
|
'port': (1024, 65535),
|
|
'assignment_ttl_seconds': (60, 7 * 24 * 60 * 60),
|
|
'max_bundle_bytes': (
|
|
1024 * 1024,
|
|
min(MAX_RESULT_BUNDLE_BYTES, config['global'].get(
|
|
'result_bundle_max_event_bytes', MAX_RESULT_BUNDLE_BYTES,
|
|
)),
|
|
),
|
|
'reaper_interval_seconds': (5, 3600),
|
|
'reaper_batch_size': (1, 1000),
|
|
'limit_concurrency': (1, 1024),
|
|
'body_idle_timeout_seconds': (1, 120),
|
|
'json_body_timeout_seconds': (1, 300),
|
|
'bundle_body_timeout_seconds': (30, 86400),
|
|
}
|
|
for name, (minimum, maximum) in integer_bounds.items():
|
|
value = worker_config.get(name)
|
|
if type(value) is not int or not minimum <= value <= maximum:
|
|
return _failure('bounds', 'config', ('supervisor', 'worker_api', name))
|
|
|
|
ttl_overrides = worker_config.get('assignment_ttl_seconds_by_source', {})
|
|
for source in worker_config['sources']:
|
|
effective_ttl = ttl_overrides.get(
|
|
source, worker_config['assignment_ttl_seconds'],
|
|
)
|
|
minimum_ttl = (
|
|
config['sources'][source]['timeout']
|
|
+ worker_config['bundle_body_timeout_seconds']
|
|
+ 60
|
|
)
|
|
if effective_ttl < minimum_ttl:
|
|
return _failure(
|
|
'bounds', 'config',
|
|
(
|
|
'supervisor', 'worker_api',
|
|
'assignment_ttl_seconds_by_source', source,
|
|
) if source in ttl_overrides else (
|
|
'supervisor', 'worker_api', 'assignment_ttl_seconds',
|
|
),
|
|
)
|
|
|
|
admin_config = worker_config.get('admin') or {}
|
|
if type(admin_config) is not dict or type(admin_config.get('enabled', False)) is not bool:
|
|
return _failure('type', 'config', ('supervisor', 'worker_api', 'admin'))
|
|
for name, default, minimum, maximum in (
|
|
('max_body_bytes', 8192, 1024, 65536),
|
|
('snapshot_limit', 200, 1, 500),
|
|
('requeue_limit', 100, 1, 500),
|
|
):
|
|
value = admin_config.get(name, default)
|
|
if type(value) is not int or not minimum <= value <= maximum:
|
|
return _failure(
|
|
'bounds', 'config', ('supervisor', 'worker_api', 'admin', name),
|
|
)
|
|
try:
|
|
managed_file_roots = managed_file_root_registry_from_config(config)
|
|
except ManagedFileConfigurationError as exc:
|
|
return _failure(
|
|
exc.category, 'config',
|
|
('supervisor', 'worker_api', 'admin', 'managed_file_roots')
|
|
+ exc.field,
|
|
)
|
|
if admin_config.get('enabled') is True:
|
|
origin = admin_config.get('origin')
|
|
marker = admin_config.get('edge_marker')
|
|
try:
|
|
parsed = urlsplit(origin)
|
|
parsed_port = parsed.port
|
|
except (TypeError, ValueError):
|
|
parsed = None
|
|
parsed_port = None
|
|
if (
|
|
type(origin) is not str
|
|
or not 1 <= len(origin) <= 512
|
|
or any(character.isspace() for character in origin)
|
|
or parsed is None
|
|
or parsed.scheme != 'https'
|
|
or not parsed.hostname
|
|
or parsed.username is not None
|
|
or parsed.password is not None
|
|
or parsed.path
|
|
or parsed.query
|
|
or parsed.fragment
|
|
or parsed_port is not None and not 1 <= parsed_port <= 65535
|
|
):
|
|
return _failure(
|
|
'bounds', 'config', ('supervisor', 'worker_api', 'admin', 'origin'),
|
|
)
|
|
if (
|
|
type(marker) is not str
|
|
or not 32 <= len(marker) <= 512
|
|
or any(character.isspace() for character in marker)
|
|
):
|
|
return _failure(
|
|
'bounds', 'config', ('supervisor', 'worker_api', 'admin', 'edge_marker'),
|
|
)
|
|
|
|
_normalized, failure = _normalize_capabilities(
|
|
package_capabilities,
|
|
worker_config['compatibility_profiles'],
|
|
worker_config['sources'],
|
|
enabled,
|
|
)
|
|
return failure
|
|
finally:
|
|
config = package_capabilities = worker_config = enabled = address_text = None
|
|
address = integer_bounds = name = minimum = maximum = value = None
|
|
source = effective_ttl = minimum_ttl = ttl_overrides = None
|
|
admin_config = default = origin = marker = None
|
|
parsed = parsed_port = _normalized = failure = managed_file_roots = None
|
|
|
|
|
|
def _expand_path(raw_value, resolved_globals):
|
|
if not _bounded_text(raw_value, MAX_PATH_BYTES) or any(
|
|
marker in raw_value for marker in ('\\', '$', '~')
|
|
):
|
|
return None
|
|
references = _PLACEHOLDER.findall(raw_value)
|
|
if len(references) > 32:
|
|
return None
|
|
if any(reference not in resolved_globals for reference in references):
|
|
return None
|
|
expanded = _PLACEHOLDER.sub(lambda match: resolved_globals[match.group(1)], raw_value)
|
|
if '{' in expanded or '}' in expanded or not expanded:
|
|
return None
|
|
if not _bounded_text(expanded, MAX_PATH_BYTES) or not expanded.startswith('/'):
|
|
return None
|
|
if posixpath.normpath(expanded) != expanded:
|
|
return None
|
|
return expanded
|
|
|
|
|
|
def _resolve_global_deployment_paths(config):
|
|
global_config = resolved = active = name = raw_value = None
|
|
try:
|
|
global_config = config['global']
|
|
resolved = {}
|
|
active = set()
|
|
|
|
def resolve(name):
|
|
raw_value = references = reference = value = None
|
|
try:
|
|
if name in resolved:
|
|
return resolved[name]
|
|
if name in active or name not in global_config or name not in _GLOBAL_PATH_FIELDS:
|
|
return None
|
|
raw_value = global_config[name]
|
|
if type(raw_value) is not str:
|
|
return None
|
|
active.add(name)
|
|
references = _PLACEHOLDER.findall(raw_value)
|
|
for reference in references:
|
|
if resolve(reference) is None:
|
|
active.remove(name)
|
|
return None
|
|
value = _expand_path(raw_value, resolved)
|
|
active.remove(name)
|
|
if value is not None:
|
|
resolved[name] = value
|
|
return value
|
|
finally:
|
|
name = raw_value = references = reference = value = None
|
|
|
|
for name in _GLOBAL_PATH_FIELDS & set(global_config):
|
|
raw_value = global_config[name]
|
|
if raw_value == '':
|
|
resolved[name] = ''
|
|
continue
|
|
if resolve(name) is None:
|
|
return None, _failure('deployment_path', 'config', ('global', name))
|
|
|
|
return resolved, None
|
|
finally:
|
|
config = global_config = resolved = active = name = raw_value = None
|
|
|
|
|
|
def _resolve_package_manifest_path(config, raw_value):
|
|
resolved = failure = path = None
|
|
try:
|
|
try:
|
|
resolved, failure = _resolve_global_deployment_paths(config)
|
|
except Exception:
|
|
return None
|
|
if failure is not None:
|
|
return None
|
|
path = _expand_path(raw_value, resolved)
|
|
if path is None or not path.startswith('/data/worker-packages/'):
|
|
return None
|
|
return path
|
|
finally:
|
|
config = raw_value = resolved = failure = path = None
|
|
|
|
|
|
def _validate_deployment_paths(config):
|
|
global_config = resolved = failure = name = expected = runtime_root = None
|
|
value = trufflehog_config = supervisor = supervisor_paths = None
|
|
supervisor_roots = root = keychecks = keycheck_paths = None
|
|
source_name = source_config = source_paths = source_roots = None
|
|
profiles = profile = path = None
|
|
try:
|
|
global_config = config['global']
|
|
resolved, failure = _resolve_global_deployment_paths(config)
|
|
if failure is not None:
|
|
return failure
|
|
|
|
for name, expected in _FIXED_PATHS.items():
|
|
if resolved.get(name) != expected:
|
|
return _failure('deployment_path', 'config', ('global', name))
|
|
|
|
def contained(path, root):
|
|
return path == root or path.startswith(root.rstrip('/') + '/')
|
|
|
|
runtime_root = resolved['runtime_dir']
|
|
for name in (
|
|
'result_spool_dir', 'legacy_result_spool_dir', 'results_dir', 'queue_dir',
|
|
'state_dir', 'log_dir', 'keycheck_dir', 'postman_cache_dir',
|
|
'gharchive_cache_dir', 'database_path', 'state_file', 'api_proxy_file',
|
|
'download_proxy_file', 'dashboard_db_path', 'scan_limiter_db',
|
|
'dockerhub_tag_cache_path', 'proxy_file',
|
|
):
|
|
value = resolved.get(name)
|
|
if value and not contained(value, runtime_root):
|
|
return _failure('deployment_path', 'config', ('global', name))
|
|
trufflehog_config = resolved.get('trufflehog_config')
|
|
if trufflehog_config and not contained(trufflehog_config, resolved['project_dir']):
|
|
return _failure('deployment_path', 'config', ('global', 'trufflehog_config'))
|
|
|
|
def validate_section(section, fields, prefix):
|
|
values = name = raw_value = value = None
|
|
try:
|
|
values = {}
|
|
for name in fields & set(section):
|
|
raw_value = section[name]
|
|
if raw_value == '':
|
|
values[name] = ''
|
|
continue
|
|
value = _expand_path(raw_value, resolved)
|
|
if value is None:
|
|
return _failure('deployment_path', 'config', prefix + (name,))
|
|
values[name] = value
|
|
return values
|
|
finally:
|
|
section = fields = prefix = values = name = raw_value = value = None
|
|
|
|
supervisor = config['supervisor']
|
|
supervisor_paths = validate_section(
|
|
supervisor, _SUPERVISOR_PATH_FIELDS, ('supervisor',),
|
|
)
|
|
if type(supervisor_paths) is tuple:
|
|
return supervisor_paths
|
|
if supervisor_paths.get('control_dir') != _FIXED_PATHS['control_dir']:
|
|
return _failure('deployment_path', 'config', ('supervisor', 'control_dir'))
|
|
supervisor_roots = {
|
|
'log_dir': resolved.get('log_dir'),
|
|
'instance_file': resolved['control_dir'],
|
|
'lock_file': resolved['control_dir'],
|
|
'supervisor_log': resolved.get('log_dir'),
|
|
'status_file': resolved.get('log_dir'),
|
|
'dashboard_log': resolved.get('log_dir'),
|
|
'state_dir': resolved.get('state_dir'),
|
|
}
|
|
for name, root in supervisor_roots.items():
|
|
value = supervisor_paths.get(name)
|
|
if value and (not root or not contained(value, root)):
|
|
return _failure('deployment_path', 'config', ('supervisor', name))
|
|
|
|
keychecks = config.get('keychecks')
|
|
if type(keychecks) is dict:
|
|
keycheck_paths = validate_section(keychecks, _KEYCHECK_PATH_FIELDS, ('keychecks',))
|
|
if type(keycheck_paths) is tuple:
|
|
return keycheck_paths
|
|
for name, value in keycheck_paths.items():
|
|
root = resolved.get('proxy_file') if name == 'proxy_file' else resolved.get('keycheck_dir')
|
|
if value and (not root or not contained(value, root)):
|
|
return _failure('deployment_path', 'config', ('keychecks', name))
|
|
for source_name, source_config in config['sources'].items():
|
|
source_paths = validate_section(
|
|
source_config, _SOURCE_PATH_FIELDS, ('sources', source_name),
|
|
)
|
|
if type(source_paths) is tuple:
|
|
return source_paths
|
|
source_roots = {
|
|
'target_file': resolved.get('queue_dir'),
|
|
'trufflehog_config': resolved.get('trufflehog_config'),
|
|
'postman_cache_dir': resolved.get('postman_cache_dir'),
|
|
'gharchive_cache_dir': resolved.get('gharchive_cache_dir'),
|
|
}
|
|
for name, value in source_paths.items():
|
|
root = source_roots[name]
|
|
if value and (not root or not contained(value, root)):
|
|
return _failure(
|
|
'deployment_path', 'config', ('sources', source_name, name),
|
|
)
|
|
|
|
profiles = supervisor['worker_api']['compatibility_profiles']
|
|
for profile in profiles.values():
|
|
path = _resolve_package_manifest_path(config, profile['package_manifest'])
|
|
if path is None:
|
|
return _failure(
|
|
'deployment_path', 'config',
|
|
('supervisor', 'worker_api', 'compatibility_profiles', 'profile', 'package_manifest'),
|
|
)
|
|
return None
|
|
finally:
|
|
config = global_config = resolved = failure = name = expected = None
|
|
runtime_root = value = trufflehog_config = supervisor = supervisor_paths = None
|
|
supervisor_roots = root = keychecks = keycheck_paths = None
|
|
source_name = source_config = source_paths = source_roots = None
|
|
profiles = profile = path = None
|
|
|
|
|
|
def _validate_runtime_semantics(config, secrets, package_capabilities):
|
|
failure = enabled_sources = supervisor_sources = None
|
|
source_name = source_config = queries = overrides = None
|
|
try:
|
|
failure = _validate_required_fields(config)
|
|
if failure is not None:
|
|
return failure
|
|
|
|
enabled_sources = config['supervisor']['enabled_sources']
|
|
if (
|
|
type(enabled_sources) is not list
|
|
or len(enabled_sources) != len(_CORE_SOURCES)
|
|
or set(enabled_sources) != set(_CORE_SOURCES)
|
|
):
|
|
return _failure('core_profile', 'config', ('supervisor', 'enabled_sources'))
|
|
config['supervisor']['enabled_sources'] = list(_CORE_SOURCES)
|
|
if any(config['sources'][source]['enabled'] is not True for source in _CORE_SOURCES):
|
|
return _failure('core_profile', 'config', ('sources',))
|
|
|
|
try:
|
|
validate_remote_assignment_capacity(config['global'])
|
|
except ValueError as exc:
|
|
return _failure('bounds', 'config', ('global', str(exc)))
|
|
|
|
supervisor_sources = config['supervisor'].get('sources', {})
|
|
if any(source not in config['sources'] for source in supervisor_sources):
|
|
return _failure('reference', 'config', ('supervisor', 'sources'))
|
|
for source_name, source_config in config['sources'].items():
|
|
queries = source_config.get('queries', [])
|
|
overrides = source_config.get('query_overrides', {})
|
|
if any(query not in queries for query in overrides):
|
|
return _failure('reference', 'config', ('sources', source_name, 'query_overrides'))
|
|
if source_config.get('target_claim_order', 'oldest') not in (
|
|
'oldest', 'newest', 'balanced',
|
|
):
|
|
return _failure(
|
|
'core_profile', 'config',
|
|
('sources', source_name, 'target_claim_order'),
|
|
)
|
|
|
|
failure = _validate_auth_references(config, secrets)
|
|
if failure is not None:
|
|
return failure
|
|
failure = _validate_worker_config(config, package_capabilities)
|
|
if failure is not None:
|
|
return failure
|
|
failure = _validate_deployment_paths(config)
|
|
if failure is not None:
|
|
return failure
|
|
|
|
try:
|
|
validate_rejected_query_policy(config)
|
|
except QueryPolicyError:
|
|
return _failure('reference', 'config', ('query_policy',))
|
|
return None
|
|
finally:
|
|
config = secrets = package_capabilities = None
|
|
failure = enabled_sources = supervisor_sources = None
|
|
source_name = source_config = queries = overrides = None
|
|
|
|
|
|
def _validate_runtime_documents_inner(config, secrets, config_template, package_capabilities):
|
|
template = normalized_config = normalized_secrets = None
|
|
template_state = state = failure = None
|
|
try:
|
|
if type(config) is not dict:
|
|
return None, _failure('mapping_root', 'config', ())
|
|
if type(config_template) is not dict:
|
|
return None, _failure('mapping_root', 'schema', ())
|
|
|
|
template_state = {'nodes': 0, 'active': set()}
|
|
template, failure = _normalize_config_shape(
|
|
config_template, config_template, (), template_state,
|
|
)
|
|
if failure is not None:
|
|
return None, _failure('schema', 'schema', ())
|
|
if _config_template_schema_hash(template) != _CONFIG_TEMPLATE_SCHEMA_SHA256:
|
|
return None, _failure('schema', 'schema', ())
|
|
state = {'nodes': 0, 'active': set()}
|
|
normalized_config, failure = _normalize_config_shape(
|
|
config, template, (), state,
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
normalized_secrets, failure = _normalize_secrets(secrets)
|
|
if failure is not None:
|
|
return None, failure
|
|
failure = _validate_runtime_semantics(
|
|
normalized_config, normalized_secrets, package_capabilities,
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
return ValidatedRuntimeDocuments(
|
|
config=normalized_config,
|
|
secrets=normalized_secrets,
|
|
), None
|
|
except Exception:
|
|
return None, _failure('schema', 'schema', ())
|
|
except BaseException:
|
|
config = secrets = config_template = package_capabilities = None
|
|
template = normalized_config = normalized_secrets = None
|
|
template_state = state = failure = None
|
|
raise
|
|
|
|
|
|
def validate_runtime_documents(
|
|
config,
|
|
secrets,
|
|
*,
|
|
config_template,
|
|
package_capabilities=None,
|
|
):
|
|
validated = failure = None
|
|
try:
|
|
validated, failure = _validate_runtime_documents_inner(
|
|
config, secrets, config_template, package_capabilities,
|
|
)
|
|
finally:
|
|
config = None
|
|
secrets = None
|
|
config_template = None
|
|
package_capabilities = None
|
|
|
|
if failure is not None:
|
|
raise RuntimeDocumentError(
|
|
failure[0], document=failure[1], path=failure[2],
|
|
)
|
|
return validated
|
|
|
|
|
|
def _preview_runtime_documents_inner(
|
|
config_payload,
|
|
secrets_payload,
|
|
config_template_payload,
|
|
package_capabilities,
|
|
):
|
|
config = secrets = config_template = validated = None
|
|
try:
|
|
config, failure = _preview_document(
|
|
config_payload, MAX_CONFIG_DOCUMENT_BYTES, 'config',
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
secrets, failure = _preview_document(
|
|
secrets_payload, MAX_SECRETS_DOCUMENT_BYTES, 'secrets',
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
config_template, failure = _preview_document(
|
|
config_template_payload, MAX_CONFIG_DOCUMENT_BYTES, 'schema',
|
|
)
|
|
if failure is not None:
|
|
return None, failure
|
|
validated, failure = _validate_runtime_documents_inner(
|
|
config, secrets, config_template, package_capabilities,
|
|
)
|
|
if failure is not None:
|
|
return None, (failure[0], None, None, failure[1], failure[2])
|
|
return validated, None
|
|
except Exception:
|
|
return None, ('schema', None, None, 'schema', 'root')
|
|
finally:
|
|
config_payload = None
|
|
secrets_payload = None
|
|
config_template_payload = None
|
|
package_capabilities = None
|
|
config = None
|
|
secrets = None
|
|
config_template = None
|
|
validated = None
|
|
|
|
|
|
def preview_runtime_documents(
|
|
config_payload,
|
|
secrets_payload,
|
|
*,
|
|
config_template_payload,
|
|
package_capabilities=None,
|
|
):
|
|
validated = failure = None
|
|
try:
|
|
validated, failure = _preview_runtime_documents_inner(
|
|
config_payload,
|
|
secrets_payload,
|
|
config_template_payload,
|
|
package_capabilities,
|
|
)
|
|
finally:
|
|
config_payload = None
|
|
secrets_payload = None
|
|
config_template_payload = None
|
|
package_capabilities = None
|
|
|
|
if failure is not None:
|
|
raise RuntimeDocumentError(
|
|
failure[0], failure[1], failure[2],
|
|
document=failure[3], path=failure[4],
|
|
)
|
|
return validated
|