|
5 | 5 | import socket |
6 | 6 | import subprocess |
7 | 7 | from datetime import datetime, timezone |
8 | | -from typing import TYPE_CHECKING, Any, Literal |
| 8 | +from typing import TYPE_CHECKING, Any |
9 | 9 |
|
10 | 10 | from pydantic import ValidationError |
11 | 11 |
|
12 | 12 | from pyoaev.exceptions import OpenAEVError |
13 | 13 | from pyoaev.signatures.models import ( |
| 14 | + CloudInjectorConfig, |
14 | 15 | ExpectationSignatureGroup, |
| 16 | + ExternalInjectorConfig, |
| 17 | + InjectorConfig, |
| 18 | + NetworkInjectorConfig, |
15 | 19 | PostExecutionSignature, |
16 | 20 | PreExecutionSignature, |
17 | 21 | SignaturePayload, |
@@ -48,120 +52,72 @@ def _utcnow(self) -> datetime: |
48 | 52 |
|
49 | 53 | def compile_pre_execution_signatures( |
50 | 54 | self, |
51 | | - inject_config: dict[str, Any], |
52 | | - category: Literal["network", "cloud", "external"], |
| 55 | + config: InjectorConfig | list[InjectorConfig], |
53 | 56 | ) -> dict[str, Any] | list[dict[str, Any]]: |
54 | | - """Build pre-execution signature dicts off the inject config and category. |
| 57 | + """Build pre-execution signature dicts from one or more typed injector configs. |
| 58 | +
|
| 59 | + The category is carried by the config type itself |
| 60 | + (:class:`NetworkInjectorConfig`, :class:`CloudInjectorConfig`, |
| 61 | + :class:`ExternalInjectorConfig`), so no separate ``category`` flag is needed. |
55 | 62 |
|
56 | 63 | Args: |
57 | | - inject_config: The inject payload dict. |
58 | | - category: One of 'network', 'cloud', 'external'. |
| 64 | + config: A single injector config or a homogeneous list of them. |
| 65 | + Multi-target injects must be expressed as a list. |
59 | 66 |
|
60 | 67 | Returns: |
61 | | - One dict for single-target, list of dicts for multi-target configs. |
| 68 | + One dict when a single config is given, otherwise a list of dicts in |
| 69 | + input order. |
62 | 70 |
|
63 | 71 | Raises: |
64 | | - ValueError: Unknown category or required fields missing. |
| 72 | + ValueError: Empty list, or mixed config types in a single call. |
| 73 | + TypeError: Unknown injector config type. |
65 | 74 | """ |
66 | | - now = self._utcnow() |
67 | | - start_time = now.strftime("%Y-%m-%dT%H:%M:%SZ") |
68 | | - |
69 | | - if category == "network": |
70 | | - return self._compile_network_pre(inject_config, start_time) |
71 | | - elif category == "cloud": |
72 | | - return self._compile_cloud_pre(inject_config, start_time) |
73 | | - elif category == "external": |
74 | | - return self._compile_external_pre(inject_config, start_time) |
75 | | - else: |
76 | | - raise ValueError(f"Unknown category: {category!r}") |
77 | | - |
78 | | - def _compile_network_pre( |
79 | | - self, config: dict[str, Any], start_time: str |
80 | | - ) -> dict[str, Any] | list[dict[str, Any]]: |
81 | | - ipv4 = self.resolve_container_ip() |
82 | | - ipv6 = self._cached_ipv6 |
83 | | - |
84 | | - assets = config.get("target_assets") or config.get("assets") or [] |
85 | | - if not assets: |
86 | | - asset = config.get("asset") |
87 | | - if asset: |
88 | | - assets = [asset] |
89 | | - |
90 | | - if not assets: |
| 75 | + configs = list(config) if isinstance(config, list) else [config] |
| 76 | + if not configs: |
91 | 77 | raise ValueError( |
92 | | - "inject_config must contain 'target_assets', 'assets', or 'asset'" |
| 78 | + "compile_pre_execution_signatures requires at least one config" |
93 | 79 | ) |
94 | 80 |
|
95 | | - results: list[dict[str, Any]] = [] |
96 | | - for asset in assets: |
97 | | - sig = PreExecutionSignature( |
98 | | - start_time=start_time, |
99 | | - source_ipv4=ipv4, |
100 | | - source_ipv6=ipv6, |
101 | | - target_ipv4=asset["target_ipv4"], |
102 | | - target_ipv6=asset.get("target_ipv6"), |
103 | | - target_hostname=asset.get("target_hostname"), |
| 81 | + first_type = type(configs[0]) |
| 82 | + if any(type(c) is not first_type for c in configs): |
| 83 | + raise ValueError( |
| 84 | + "compile_pre_execution_signatures does not mix injector config types; " |
| 85 | + f"got {sorted({type(c).__name__ for c in configs})}" |
104 | 86 | ) |
105 | | - results.append(sig.model_dump(mode="json", exclude_none=True)) |
106 | 87 |
|
| 88 | + start_time = self._utcnow().strftime("%Y-%m-%dT%H:%M:%SZ") |
| 89 | + results = [self._compile_one(cfg, start_time) for cfg in configs] |
107 | 90 | return results[0] if len(results) == 1 else results |
108 | 91 |
|
109 | | - def _compile_cloud_pre( |
110 | | - self, config: dict[str, Any], start_time: str |
111 | | - ) -> dict[str, Any] | list[dict[str, Any]]: |
112 | | - cloud_provider = config["cloud_provider"] |
113 | | - cloud_account_id = config["cloud_account_id"] |
114 | | - target_service = config.get("target_service") |
115 | | - |
116 | | - regions = config.get("regions") or [] |
117 | | - if not regions: |
118 | | - region = config.get("cloud_region") |
119 | | - if region: |
120 | | - regions = [region] |
121 | | - |
122 | | - if not regions: |
123 | | - raise ValueError("inject_config must contain 'regions' or 'cloud_region'") |
124 | | - |
125 | | - results: list[dict[str, Any]] = [] |
126 | | - for region in regions: |
127 | | - sig = PreExecutionSignature( |
128 | | - start_time=start_time, |
129 | | - cloud_provider=cloud_provider, |
130 | | - cloud_account_id=cloud_account_id, |
131 | | - cloud_region=region, |
132 | | - target_service=target_service, |
133 | | - ) |
134 | | - results.append(sig.model_dump(mode="json", exclude_none=True)) |
| 92 | + def _compile_one(self, config: InjectorConfig, start_time: str) -> dict[str, Any]: |
| 93 | + """Project a single injector config into a flat pre-execution signature dict. |
135 | 94 |
|
136 | | - return results[0] if len(results) == 1 else results |
137 | | - |
138 | | - def _compile_external_pre( |
139 | | - self, config: dict[str, Any], start_time: str |
140 | | - ) -> dict[str, Any] | list[dict[str, Any]]: |
141 | | - targets = config.get("targets") or [] |
142 | | - if not targets: |
143 | | - query = config.get("query") |
144 | | - if query is None: |
145 | | - raise ValueError("inject_config must contain 'query'") |
146 | | - targets = [ |
147 | | - { |
148 | | - "target_ipv4": config["target_ipv4"], |
149 | | - "target_hostname": config.get("target_hostname"), |
150 | | - "query": query, |
151 | | - } |
152 | | - ] |
| 95 | + Common pipeline for every category: |
| 96 | + 1. Seed the base dict with ``start_time`` and category-specific context |
| 97 | + (network gets resolved source IPs; cloud/external add nothing). |
| 98 | + 2. Layer the config's own fields on top. |
| 99 | + 3. Run it through :class:`PreExecutionSignature` for validation |
| 100 | + and emit JSON-ready output stripped of ``None``\\ s. |
| 101 | + """ |
| 102 | + base: dict[str, Any] = {"start_time": start_time} |
| 103 | + base.update(self._source_context(config)) |
| 104 | + base.update(config.model_dump(exclude_none=True)) |
| 105 | + return PreExecutionSignature(**base).model_dump(mode="json", exclude_none=True) |
153 | 106 |
|
154 | | - results: list[dict[str, Any]] = [] |
155 | | - for target in targets: |
156 | | - sig = PreExecutionSignature( |
157 | | - start_time=start_time, |
158 | | - target_ipv4=target["target_ipv4"], |
159 | | - target_hostname=target.get("target_hostname"), |
160 | | - query=target.get("query"), |
161 | | - ) |
162 | | - results.append(sig.model_dump(mode="json", exclude_none=True)) |
| 107 | + def _source_context(self, config: InjectorConfig) -> dict[str, Any]: |
| 108 | + """Return the source identity bits injected for the config's category. |
163 | 109 |
|
164 | | - return results[0] if len(results) == 1 else results |
| 110 | + Only network signatures need the running container's source IPs; |
| 111 | + cloud and external rows have no source identity to carry. |
| 112 | + """ |
| 113 | + if isinstance(config, NetworkInjectorConfig): |
| 114 | + return { |
| 115 | + "source_ipv4": self.resolve_container_ip(), |
| 116 | + "source_ipv6": self._cached_ipv6, |
| 117 | + } |
| 118 | + if isinstance(config, (CloudInjectorConfig, ExternalInjectorConfig)): |
| 119 | + return {} |
| 120 | + raise TypeError(f"unsupported injector config type: {type(config).__name__}") |
165 | 121 |
|
166 | 122 | def compile_post_execution_signatures( |
167 | 123 | self, |
|
0 commit comments