[client-python] feat(signature): first US for ExpectationSignature (#206)#257
[client-python] feat(signature): first US for ExpectationSignature (#206)#257Kakudou wants to merge 14 commits into
Conversation
|
Initial message edited to reflect the new behavior based on review/usages. Also as for the NetworkInjectorConfig, we can use a builder to quickly create them from a list of targets (from build_network_configs(["10.0.0.1", "2001:db8::1", "web.example.com"]) |
|
|
||
|
|
||
| class NetworkInjectorConfig(BaseModel): | ||
| """A single network target. Exactly one of ``target_ipv4``, ``target_ipv6``, or ``target_hostname``.""" |
There was a problem hiding this comment.
Should we use a pydantic model_validator (eventually a mode=before) to ensure there is at least one but only one of those three options?
Something like
@model_validator(mode='before')
def check_one(cls, data):
assert sum(value != None for key, value in data.items() if key in ['target_ipv4', 'target_ipv6', 'target_hostname']) == 1
return data
That's the first US to introduce the SignatureExpectation for the injectors.
Proposed changes
SignatureManager
Unified signature lifecycle for OpenAEV injectors: compile pre-execution signatures, merge post-execution results, and ship structured output to the backend.
Architecture
flowchart LR subgraph pyoaev/signatures SM[SignatureManager] M[models.py] CFG["InjectorConfig\n(Network / Cloud / External)"] end subgraph pyoaev/apis API[SignatureApiManager] end subgraph Backend CB["/api/injects/{id}/callback"] end CFG -->|typed input| SM SM -->|compile_pre/post| M SM -->|send_signatures| API API -->|callback\nretry + chunk| CBInjector configs (
models.py) are the typed contract: one config = one signature row.SignatureManager owns the domain logic (compile, merge, resolve IP).
SignatureApiManager owns the transport (validation, chunking, retry).
Quick Start
Injector configs
The category is encoded in the config type. Pass a single config for a single-target inject,
or a homogeneous list for a multi-target inject. Mixing config types in a single call is rejected.
NetworkInjectorConfigtarget_ipv4/target_ipv6/target_hostnameCloudInjectorConfigcloud_provider,cloud_account_id,cloud_regiontarget_serviceExternalInjectorConfigquerytarget_ipv4,target_hostnameInjectorConfigis the union type:NetworkInjectorConfig | CloudInjectorConfig | ExternalInjectorConfig.SignatureManageraddsstart_timeautomatically (plussource_ipv4/source_ipv6for network).Network
####### Network builder
build_network_configs(targets)turns a heterogeneous list of strings, dicts, or already-typedNetworkInjectorConfiginto a clean list of typed configs. Strings are auto-classified intoIPv4 / IPv6 / hostname via the stdlib
ipaddressmodule. Each input is treated as one distinctasset — a target never mixes identities.
Cloud
External
Compiled output shapes
compile_pre_execution_signaturesreturns a single flat dict for one config, or a list of dictsfor a list of configs.
Nonefields are stripped.compile_post_execution_signatures(pre, tool_output)preserves the input shape (dict in, dict out;list in, list out) and adds
end_time,execution_status, and optionalpartial_results.Anything in
tool_output["extra_signatures"]is merged into the final dict verbatim, useful forinjector-specific fields like
parent_process_nameor custom signal types.Failure modes
compile_pre_execution_signaturesValueErrorNetwork+Cloud)ValueErrorNetworkInjectorConfigwith zero or more than one identity fieldValidationErrorbuild_network_configsitem that's neitherstr,dict, nor aNetworkInjectorConfigTypeErrortool_outputin post-executionOpenAEVErrorLifecycle Flow
sequenceDiagram participant Injector participant SM as SignatureManager participant API as SignatureApiManager participant Backend Injector->>SM: compile_pre_execution_signatures(config) SM-->>Injector: pre_signatures dict/list Note over Injector: Tool executes... Injector->>SM: compile_post_execution_signatures(pre, tool_output) SM-->>Injector: merged signatures Injector->>SM: build_payload(post, target_meta, expectation_type) SM-->>Injector: nested wire payload Injector->>SM: send_signatures(inject_id, phase, signatures) SM->>API: send_signatures(inject_id, phase, signatures) API->>API: validate + normalize + chunk if needed API->>Backend: POST /api/injects/{id}/callback Backend-->>API: 200/202Transport Behaviour
max_payload_size(default 1 MiB) are split by target and sent sequentially withchunk_index/total_chunksmetadata.SignatureTransmissionErrorimmediately.Wire Format
Payloads follow the nested schema expected by the callback endpoint:
{ "phase": "execution_complete", "expectation_signature": { "targets": [ { "signature_target": { "agent": "...", "asset": "...", "asset_group": "..." }, "signature_values": [ { "expectation_type": "DETECTION", "values": [ { "signature_type": "source_ipv4", "signature_value": "172.17.0.2" }, { "signature_type": "target_ipv4", "signature_value": "10.0.0.1" }, { "signature_type": "start_time", "signature_value": "2024-06-26T06:06:06Z" }, { "signature_type": "end_time", "signature_value": "2024-06-26T06:06:09Z" }, { "signature_type": "execution_status", "signature_value": "success" } ] } ] } ] } }Known
signature_typelabels live inpyoaev.signatures.SignatureTypes(
source_ipv4_address,target_hostname_address,cloud_region,query, ...). The wire formatitself accepts any string, so injectors are free to add custom types via
tool_output.extra_signatures.Utility
Resolution strategy:
CONTAINER_IPenv var >socket.gethostbyname>hostname -i>"unknown".The result is cached for the lifetime of the manager and IPv6 is sniffed best-effort alongside.
Related issues
ContractOutputType:ExpectationSignature#206Checklist
Further comments